attractor 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
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