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 +4 -4
- data/README.md +18 -0
- data/lib/attractor.rb +16 -0
- data/lib/attractor.rb~ +1 -0
- data/lib/attractor/cache.rb +15 -0
- data/lib/attractor/cache.rb~ +67 -0
- data/lib/attractor/calculators/base_calculator.rb +10 -1
- data/lib/attractor/calculators/base_calculator.rb~ +18 -6
- data/lib/attractor/cli.rb +15 -0
- data/lib/attractor/cli.rb~ +84 -0
- data/lib/attractor/duration_parser.rb +3 -1
- data/lib/attractor/duration_parser.rb~ +20 -16
- data/lib/attractor/reporters/base_reporter.rb +0 -1
- data/lib/attractor/version.rb +1 -1
- metadata +3 -3
- data/lib/attractor/#duration_parser.rb# +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b116bbaa914ca032391378e04b8e41f27b28fa4feaca3fc50c93053d1954a15a
|
4
|
+
data.tar.gz: 3e5620d661d19d3d2308457921389ce78b4ba3b844a6a4d3dd33c1ef9d093a56
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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")
|
data/lib/attractor/cache.rb
CHANGED
@@ -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
|
data/lib/attractor/cache.rb~
CHANGED
@@ -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].
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
@@ -1,23 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Attractor
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
-
|
13
|
+
attr_reader :duration
|
10
14
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
15
|
+
def initialize(input)
|
16
|
+
@input = input
|
17
|
+
@duration = 0
|
18
|
+
parse
|
19
|
+
end
|
16
20
|
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
data/lib/attractor/version.rb
CHANGED
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.
|
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-
|
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
|