attractor 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 38e9529df9d6404034cde6792aab2f1dec600e1d13269b819f71074003a4162b
4
- data.tar.gz: 319ad98e23caed681ec07278c7d27402f080618810dab2532aa79ac9c0778c4c
3
+ metadata.gz: b116bbaa914ca032391378e04b8e41f27b28fa4feaca3fc50c93053d1954a15a
4
+ data.tar.gz: 3e5620d661d19d3d2308457921389ce78b4ba3b844a6a4d3dd33c1ef9d093a56
5
5
  SHA512:
6
- metadata.gz: aae211d1f8f4519a5470b43a6ceefdf3ea40580bfbb524e14e13c79ab8f99dc95269e28c089a114c60628ac58eb43ff299c87cc021ce72273a855ef0de21b4ef
7
- data.tar.gz: 304a7d902b3fdb6440068ba2501dbe1b9b7cd1b7fea3b1a3590533ab6654a7575ecedd1109fe94e6afb62be0d734f3ae1292f56231a8a157eec79d8ecebbaa07
6
+ metadata.gz: '092ff47f81f9d6f0e7fa025d21a89501413e1dc8c7044d5420d4db8ea04f72fc6554840a9656b0ac002c2f0c25ef3f0fa9c1204214d4b29a284eabb565428f82'
7
+ data.tar.gz: 83ad8a7dafa969e68c4db77629663738513137a3f91ad058451d9a37c1ed39aff1cfea785c34042d543b7caac8ec625207cf3701c7003d63a23f631cbba109b5
data/README.md CHANGED
@@ -186,6 +186,17 @@ attractor:
186
186
 
187
187
  ## CLI Commands and Options
188
188
 
189
+ Initialize the local cache:
190
+
191
+ ```sh
192
+ attractor init
193
+ --file_prefix|-p app/models
194
+ --type|-t rb|js
195
+ --start_ago|-s (e.g. 5y, 3m, 7w)
196
+ --minimum_churn|-c (minimum times a file must have changed to be processed)
197
+ --ignore|-i 'spec/*_spec.rb,db/schema.rb,tmp'
198
+ ```
199
+
189
200
  Print a simple output to console:
190
201
 
191
202
  ```sh
@@ -223,6 +234,13 @@ attractor serve
223
234
  --ignore|-i 'spec/*_spec.rb,db/schema.rb,tmp'
224
235
  ```
225
236
 
237
+ Clear the local cache:
238
+
239
+ ```sh
240
+ attractor clean
241
+ ```
242
+
243
+
226
244
  ## Development
227
245
 
228
246
  After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/lib/attractor.rb CHANGED
@@ -21,6 +21,15 @@ module Attractor
21
21
 
22
22
  @registry_entries = {}
23
23
 
24
+ def init(calculators)
25
+ calculators ||= all_registered_calculators
26
+ calculators.to_a.map(&:last).each(&:calculate)
27
+ end
28
+
29
+ def clear
30
+ Cache.clear
31
+ end
32
+
24
33
  def register(registry_entry)
25
34
  @registry_entries[registry_entry.type] = registry_entry
26
35
  end
@@ -30,13 +39,20 @@ module Attractor
30
39
 
31
40
  return {type => registry_entry_for_type.calculator_class.new(**options)} if type
32
41
 
42
+ all_registered_calculators(**options)
43
+ end
44
+
45
+ def all_registered_calculators(options = {})
33
46
  Hash[@registry_entries.map do |type, entry|
34
47
  [type, entry.calculator_class.new(**options)] if entry.detector_class.new.detect
35
48
  end.compact]
36
49
  end
37
50
 
38
51
  module_function :calculators_for_type
52
+ module_function :all_registered_calculators
39
53
  module_function :register
54
+ module_function :init
55
+ module_function :clear
40
56
  end
41
57
 
42
58
  Attractor::GemNames.new.to_a.each do |gem_name|
data/lib/attractor.rb~ CHANGED
@@ -8,6 +8,7 @@ require "attractor/detectors/base_detector"
8
8
  require "attractor/reporters/base_reporter"
9
9
  require "attractor/suggester"
10
10
  require "attractor/watcher"
11
+ require "attractor/cache"
11
12
 
12
13
  Dir[File.join(__dir__, "attractor", "reporters", "*.rb")].sort.each do |file|
13
14
  next if file.start_with?("base")
@@ -14,6 +14,14 @@ module Attractor
14
14
  adapter.write(file_path: file_path, value: value)
15
15
  end
16
16
 
17
+ def persist!
18
+ adapter.persist!
19
+ end
20
+
21
+ def clear
22
+ adapter.clear
23
+ end
24
+
17
25
  private
18
26
 
19
27
  def adapter
@@ -56,9 +64,16 @@ module Attractor
56
64
 
57
65
  transformed_value = value.to_h.transform_keys { |k| mappings[k] || k }
58
66
  @store[file_path] = {value.current_commit => transformed_value}
67
+ end
68
+
69
+ def persist!
59
70
  File.write(filename, ::JSON.dump(@store))
60
71
  end
61
72
 
73
+ def clear
74
+ FileUtils.rm filename
75
+ end
76
+
62
77
  def filename
63
78
  "#{@data_directory}/attractor-cache.json"
64
79
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "psych"
5
+
6
+ module Attractor
7
+ class Cache
8
+ class << self
9
+ def read(file_path:)
10
+ adapter.read(file_path: file_path)
11
+ end
12
+
13
+ def write(file_path:, value:)
14
+ adapter.write(file_path: file_path, value: value)
15
+ end
16
+
17
+ private
18
+
19
+ def adapter
20
+ @@adapter ||= CacheAdapter::JSON.instance
21
+ end
22
+ end
23
+ end
24
+
25
+ module CacheAdapter
26
+ class Base
27
+ include Singleton
28
+ end
29
+
30
+ class JSON < Base
31
+ def initialize
32
+ super
33
+
34
+ @data_directory = "tmp"
35
+ FileUtils.mkdir_p @data_directory
36
+ FileUtils.touch filename
37
+
38
+ begin
39
+ @store = ::JSON.parse(File.read(filename))
40
+ rescue ::JSON::ParserError
41
+ @store = {}
42
+ end
43
+ end
44
+
45
+ def read(file_path:)
46
+ value_hash = @store[file_path]
47
+
48
+ Value.new(**value_hash.values.first.transform_keys(&:to_sym)) unless value_hash.nil?
49
+ rescue ArgumentError => e
50
+ puts "Couldn't rehydrate value from cache: #{e.message}"
51
+ nil
52
+ end
53
+
54
+ def write(file_path:, value:)
55
+ mappings = {x: :churn, y: :complexity}
56
+
57
+ transformed_value = value.to_h.transform_keys { |k| mappings[k] || k }
58
+ @store[file_path] = {value.current_commit => transformed_value}
59
+ File.write(filename, ::JSON.dump(@store))
60
+ end
61
+
62
+ def filename
63
+ "#{@data_directory}/attractor-cache.json"
64
+ end
65
+ end
66
+ end
67
+ end
@@ -26,7 +26,9 @@ module Attractor
26
26
  ignores: @ignores
27
27
  ).report(false)
28
28
 
29
- churn[:churn][:changes].map do |change|
29
+ puts "Calculating churn and complexity values for #{churn[:churn][:changes].size} #{type} files"
30
+
31
+ values = churn[:churn][:changes].map do |change|
30
32
  history = git_history_for_file(file_path: change[:file_path])
31
33
  commit = history&.first&.first
32
34
 
@@ -45,8 +47,15 @@ module Attractor
45
47
  Cache.write(file_path: change[:file_path], value: value)
46
48
  end
47
49
 
50
+ print "."
48
51
  value
49
52
  end
53
+
54
+ Cache.persist!
55
+
56
+ print "\n\n"
57
+
58
+ values
50
59
  end
51
60
 
52
61
  private
@@ -27,12 +27,24 @@ module Attractor
27
27
  ).report(false)
28
28
 
29
29
  churn[:churn][:changes].map do |change|
30
- complexity, details = yield(change)
31
- value = Value.new(file_path: change[:file_path],
32
- churn: change[:times_changed],
33
- complexity: complexity,
34
- details: details,
35
- history: git_history_for_file(file_path: change[:file_path]))
30
+ history = git_history_for_file(file_path: change[:file_path])
31
+ commit = history&.first&.first
32
+
33
+ cached_value = Cache.read(file_path: change[:file_path])
34
+
35
+ if !cached_value.nil? && !cached_value.current_commit.nil? && cached_value.current_commit == commit
36
+ value = cached_value
37
+ else
38
+ complexity, details = yield(change)
39
+
40
+ value = Value.new(file_path: change[:file_path],
41
+ churn: change[:times_changed],
42
+ complexity: complexity,
43
+ details: details,
44
+ history: history)
45
+ Cache.write(file_path: change[:file_path], value: value)
46
+ end
47
+
36
48
  value
37
49
  end
38
50
  end
data/lib/attractor/cli.rb CHANGED
@@ -26,6 +26,21 @@ module Attractor
26
26
  puts "Runtime error: #{e.message}"
27
27
  end
28
28
 
29
+ desc "clean", "Clears attractor's cache"
30
+ def clean
31
+ puts "Clearing attractor cache"
32
+ Attractor.clear
33
+ end
34
+
35
+ desc "init", "Initializes attractor's cache"
36
+ shared_options.each do |shared_option|
37
+ option(*shared_option)
38
+ end
39
+ def init
40
+ puts "Warming attractor cache"
41
+ Attractor.init(calculators(options))
42
+ end
43
+
29
44
  desc "calc", "Calculates churn and complexity for all ruby files in current directory"
30
45
  shared_options.each do |shared_option|
31
46
  option(*shared_option)
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ require "attractor"
6
+
7
+ module Attractor
8
+ # contains methods implementing the CLI
9
+ class CLI < Thor
10
+ shared_options = [[:file_prefix, aliases: :p],
11
+ [:ignore, aliases: :i, default: ""],
12
+ [:watch, aliases: :w, type: :boolean],
13
+ [:minimum_churn, aliases: :c, type: :numeric, default: 3],
14
+ [:start_ago, aliases: :s, type: :string, default: "5y"],
15
+ [:type, aliases: :t]]
16
+
17
+ advanced_options = [[:format, aliases: :f, default: "html"],
18
+ [:no_open_browser, type: :boolean],
19
+ [:ci, type: :boolean]]
20
+
21
+ desc "version", "Prints Attractor's version information"
22
+ map %w[-v --version] => :version
23
+ def version
24
+ puts "Attractor version #{Attractor::VERSION}"
25
+ rescue RuntimeError => e
26
+ puts "Runtime error: #{e.message}"
27
+ end
28
+
29
+ desc "calc", "Calculates churn and complexity for all ruby files in current directory"
30
+ shared_options.each do |shared_option|
31
+ option(*shared_option)
32
+ end
33
+ def calc
34
+ file_prefix = options[:file_prefix]
35
+
36
+ report! Attractor::ConsoleReporter.new(file_prefix: file_prefix, ignores: options[:ignore], calculators: calculators(options))
37
+ rescue RuntimeError => e
38
+ puts "Runtime error: #{e.message}"
39
+ end
40
+
41
+ desc "report", "Generates an HTML report"
42
+ (shared_options + advanced_options).each do |option|
43
+ option(*option)
44
+ end
45
+ def report
46
+ file_prefix = options[:file_prefix]
47
+ open_browser = !(options[:no_open_browser] || options[:ci])
48
+
49
+ report! Attractor::HtmlReporter.new(file_prefix: file_prefix, ignores: options[:ignore], calculators: calculators(options), open_browser: open_browser)
50
+ rescue RuntimeError => e
51
+ puts "Runtime error: #{e.message}"
52
+ end
53
+
54
+ desc "serve", "Serves the report on localhost"
55
+ (shared_options + advanced_options).each do |option|
56
+ option(*option)
57
+ end
58
+ def serve
59
+ file_prefix = options[:file_prefix]
60
+ open_browser = !(options[:no_open_browser] || options[:ci])
61
+
62
+ report! Attractor::SinatraReporter.new(file_prefix: file_prefix, ignores: options[:ignore], calculators: calculators(options), open_browser: open_browser)
63
+ end
64
+
65
+ private
66
+
67
+ def calculators(options)
68
+ Attractor.calculators_for_type(options[:type],
69
+ file_prefix: options[:file_prefix],
70
+ minimum_churn_count: options[:minimum_churn],
71
+ ignores: options[:ignore],
72
+ start_ago: options[:start_ago])
73
+ end
74
+
75
+ def report!(reporter)
76
+ if options[:watch]
77
+ puts "Listening for file changes..."
78
+ reporter.watch
79
+ else
80
+ reporter.report
81
+ end
82
+ end
83
+ end
84
+ end
@@ -14,7 +14,9 @@ module Attractor
14
14
 
15
15
  def initialize(input)
16
16
  @input = input
17
- @duration = 0
17
+ @duration = @input.is_a?(Numeric) ? @input : 0
18
+ return if @duration > 0
19
+
18
20
  parse
19
21
  end
20
22
 
@@ -1,23 +1,27 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Attractor
2
- class DurationParser
3
- TOKENS = {
4
- "m" => (60),
5
- "h" => (60 * 60),
6
- "d" => (60 * 60 * 24)
7
- }
4
+ # converts a duration string into an amount of days
5
+ class DurationParser
6
+ TOKENS = {
7
+ "d" => 1,
8
+ "w" => 7,
9
+ "m" => 30,
10
+ "y" => 365
11
+ }.freeze
8
12
 
9
- attr_reader :time
13
+ attr_reader :duration
10
14
 
11
- def initialize(input)
12
- @input = input
13
- @time = 0
14
- parse
15
- end
15
+ def initialize(input)
16
+ @input = input
17
+ @duration = 0
18
+ parse
19
+ end
16
20
 
17
- def parse
18
- @input.scan(/(\d+)(\w)/).each do |amount, measure|
19
- @time += amount.to_i * TOKENS[measure]
21
+ def parse
22
+ @input.scan(/(\d+)(\w)/).each do |amount, measure|
23
+ @duration += amount.to_i * TOKENS[measure]
24
+ end
20
25
  end
21
26
  end
22
27
  end
23
- end
@@ -19,7 +19,6 @@ module Attractor
19
19
  @file_prefix = file_prefix || ""
20
20
  @calculators = calculators
21
21
  @open_browser = open_browser
22
- @values = @calculators.first.last.calculate
23
22
  @suggester = Suggester.new(values)
24
23
 
25
24
  @watcher = Watcher.new(@file_prefix, ignores, lambda do
@@ -1,3 +1,3 @@
1
1
  module Attractor
2
- VERSION = "2.2.0"
2
+ VERSION = "2.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attractor
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julian Rubisch
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-04-09 00:00:00.000000000 Z
11
+ date: 2021-04-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: churn
@@ -372,12 +372,12 @@ files:
372
372
  - exe/attractor
373
373
  - lib/attractor.rb
374
374
  - lib/attractor.rb~
375
- - lib/attractor/#duration_parser.rb#
376
375
  - lib/attractor/cache.rb
377
376
  - lib/attractor/cache.rb~
378
377
  - lib/attractor/calculators/base_calculator.rb
379
378
  - lib/attractor/calculators/base_calculator.rb~
380
379
  - lib/attractor/cli.rb
380
+ - lib/attractor/cli.rb~
381
381
  - lib/attractor/detectors/base_detector.rb
382
382
  - lib/attractor/detectors/base_detector.rb~
383
383
  - lib/attractor/duration_parser.rb
@@ -1,24 +0,0 @@
1
- module Attractor
2
- class DurationParser
3
- TOKENS = {
4
- 'd" => 1,
5
- w" => 7,
6
- m" => 30,
7
- y" => 365
8
- }
9
-
10
- attr_reader :duration
11
-
12
- def initialize(input)
13
- @input = input
14
- @duration = 0
15
- parse
16
- end
17
-
18
- def parse
19
- @input.scan(/(\d+)(\w)/).each do |amount, measure|
20
- @duration += amount.to_i * TOKENS[measure]
21
- end
22
- end
23
- end
24
- end