attractor 2.7.0 → 2.7.1
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 +24 -0
- data/lib/attractor/calculators/base_calculator.rb +4 -1
- data/lib/attractor/diff_calculator.rb +19 -4
- data/lib/attractor/formatters/formats/json.rb +1 -0
- data/lib/attractor/formatters/formats/markdown.rb +47 -5
- data/lib/attractor/formatters/report.rb +4 -2
- data/lib/attractor/formatters/targets/diff.rb +63 -8
- data/lib/attractor/reporters/console_reporter.rb +1 -0
- data/lib/attractor/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f92fa88c85c572827d8a18feba00fcbd156c70a4642bbb9544beb90d7242af8f
|
|
4
|
+
data.tar.gz: d146f48f098cfb0e12ea135ed05279e1accf7ad44be1dbdd7009f8a1441efe60
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 379f288757d88603c45617ffc626c14498bcefc6e14c6442b1b95adf85afef07bb8b064833b2ceed36ebd582f6b0fafe2799fe972593f56f8755a757c330cf90
|
|
7
|
+
data.tar.gz: c89dcdb9f88391c072bec3af06f2efa2c962b70e95e04bbcc7899ba5dc61473d0461c7d23d9ebf59479c60a99d5c3d2e5dac6394cc427a352c96f4881ac56a25
|
data/README.md
CHANGED
|
@@ -238,6 +238,30 @@ attractor calc
|
|
|
238
238
|
--config path/to/.attractor.yml
|
|
239
239
|
```
|
|
240
240
|
|
|
241
|
+
Calculate the complexity delta between two git refs:
|
|
242
|
+
|
|
243
|
+
```sh
|
|
244
|
+
attractor diff
|
|
245
|
+
--base main
|
|
246
|
+
--head HEAD
|
|
247
|
+
--file_prefix|-p app/models
|
|
248
|
+
--type|-t rb|js
|
|
249
|
+
--start_ago|-s (e.g. 5y, 3m, 7w)
|
|
250
|
+
--minimum_churn|-c (minimum times a file must have changed to be processed)
|
|
251
|
+
--ignore|-i 'spec/*_spec.rb,db/schema.rb,tmp'
|
|
252
|
+
--files 'app/models/user.rb,app/models/post.rb'
|
|
253
|
+
--files -
|
|
254
|
+
--format|-f table|markdown|json
|
|
255
|
+
--config path/to/.attractor.yml
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The `markdown` format produces a PR/MR-ready summary with Stats, Trends, and To-dos sections, followed by the full per-file table wrapped in a collapsible `<details>` block. Pipe it into a GitHub or GitLab comment:
|
|
259
|
+
|
|
260
|
+
```sh
|
|
261
|
+
attractor diff --base main --head HEAD --format markdown | gh pr comment 123 --body-file -
|
|
262
|
+
attractor diff --base main --head HEAD --format markdown | glab mr note 45 --message "$(cat)"
|
|
263
|
+
```
|
|
264
|
+
|
|
241
265
|
Generate a full report
|
|
242
266
|
|
|
243
267
|
```sh
|
|
@@ -34,7 +34,10 @@ module Attractor
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
target_paths = if @files && !@files.empty?
|
|
37
|
-
|
|
37
|
+
# `file_extension` is a pattern, not a literal: the JavaScript calculator
|
|
38
|
+
# passes "(js|jsx)". `end_with?` would drop every file it lists.
|
|
39
|
+
extension = /\.(?:#{@file_extension})\z/
|
|
40
|
+
@files.uniq.select { |file_path| file_path.match?(extension) }
|
|
38
41
|
else
|
|
39
42
|
changes_by_path.keys
|
|
40
43
|
end
|
|
@@ -62,12 +62,13 @@ module Attractor
|
|
|
62
62
|
def build_diff(base_values, head_values, files)
|
|
63
63
|
base_by_path = flatten_values(base_values)
|
|
64
64
|
head_by_path = flatten_values(head_values)
|
|
65
|
+
file_types = build_file_types(base_values, head_values)
|
|
65
66
|
|
|
66
67
|
base_refactor_files = refactor_files(base_by_path.values)
|
|
67
68
|
head_refactor_files = refactor_files(head_by_path.values)
|
|
68
69
|
|
|
69
70
|
rows = files.uniq.map do |file_path|
|
|
70
|
-
build_row(file_path, base_by_path[file_path], head_by_path[file_path], base_refactor_files, head_refactor_files)
|
|
71
|
+
build_row(file_path, base_by_path[file_path], head_by_path[file_path], base_refactor_files, head_refactor_files, file_types[file_path])
|
|
71
72
|
end
|
|
72
73
|
|
|
73
74
|
rows.sort_by! { |row| -row[:delta].abs }
|
|
@@ -86,8 +87,20 @@ module Attractor
|
|
|
86
87
|
end
|
|
87
88
|
|
|
88
89
|
def flatten_values(values_by_type)
|
|
89
|
-
values_by_type.
|
|
90
|
-
|
|
90
|
+
values_by_type.each_with_object({}) do |(_type, values), hash|
|
|
91
|
+
Array(values).compact.each do |value|
|
|
92
|
+
hash[value.file_path] = value
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def build_file_types(base_values, head_values)
|
|
98
|
+
[base_values, head_values].each_with_object({}) do |values_by_type, hash|
|
|
99
|
+
values_by_type.each do |type, values|
|
|
100
|
+
Array(values).compact.each do |value|
|
|
101
|
+
hash[value.file_path] ||= type
|
|
102
|
+
end
|
|
103
|
+
end
|
|
91
104
|
end
|
|
92
105
|
end
|
|
93
106
|
|
|
@@ -95,7 +108,7 @@ module Attractor
|
|
|
95
108
|
Suggester.new(values).suggest.map(&:file_path)
|
|
96
109
|
end
|
|
97
110
|
|
|
98
|
-
def build_row(file_path, base_value, head_value, base_refactor_files, head_refactor_files)
|
|
111
|
+
def build_row(file_path, base_value, head_value, base_refactor_files, head_refactor_files, type)
|
|
99
112
|
complexity_base = base_value&.complexity
|
|
100
113
|
complexity_head = head_value&.complexity
|
|
101
114
|
|
|
@@ -112,10 +125,12 @@ module Attractor
|
|
|
112
125
|
|
|
113
126
|
{
|
|
114
127
|
file_path: file_path,
|
|
128
|
+
type: type,
|
|
115
129
|
complexity_base: complexity_base,
|
|
116
130
|
complexity_head: complexity_head,
|
|
117
131
|
delta: delta,
|
|
118
132
|
churn: head_value&.churn,
|
|
133
|
+
score_base: base_value&.score,
|
|
119
134
|
score_head: head_value&.score,
|
|
120
135
|
refactor_base: base_refactor_files.include?(file_path),
|
|
121
136
|
refactor_head: head_refactor_files.include?(file_path),
|
|
@@ -6,7 +6,7 @@ module Attractor
|
|
|
6
6
|
class Markdown
|
|
7
7
|
def call(report)
|
|
8
8
|
lines = []
|
|
9
|
-
lines << "# #{report.title}"
|
|
9
|
+
lines << "#{title_prefix(report)} #{report.title}"
|
|
10
10
|
lines << ""
|
|
11
11
|
|
|
12
12
|
report.metadata.each do |key, value|
|
|
@@ -14,11 +14,24 @@ module Attractor
|
|
|
14
14
|
end
|
|
15
15
|
lines << "" unless report.metadata.empty?
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
report.sections.each do |section|
|
|
18
|
+
lines << "### #{section[:title]}"
|
|
19
|
+
lines << ""
|
|
20
|
+
lines += render_table(section[:columns], section[:rows])
|
|
21
|
+
lines << ""
|
|
22
|
+
end
|
|
19
23
|
|
|
20
|
-
report.
|
|
21
|
-
lines << "
|
|
24
|
+
if report.collapsed_table
|
|
25
|
+
lines << "<details>"
|
|
26
|
+
lines << "<summary>All files</summary>"
|
|
27
|
+
lines << ""
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
lines += render_table(report.columns, report.rows)
|
|
31
|
+
|
|
32
|
+
if report.collapsed_table
|
|
33
|
+
lines << ""
|
|
34
|
+
lines << "</details>"
|
|
22
35
|
end
|
|
23
36
|
|
|
24
37
|
if report.footer
|
|
@@ -31,6 +44,35 @@ module Attractor
|
|
|
31
44
|
|
|
32
45
|
private
|
|
33
46
|
|
|
47
|
+
def render_table(columns, rows)
|
|
48
|
+
return [] if columns.empty?
|
|
49
|
+
|
|
50
|
+
lines = []
|
|
51
|
+
lines << "| #{columns.join(" | ")} |"
|
|
52
|
+
lines << "| #{columns.map { |column| "-" * column.to_s.length }.join(" | ")} |"
|
|
53
|
+
|
|
54
|
+
rows.each do |row|
|
|
55
|
+
lines << "| #{columns.map { |column| format_cell(column, row) }.join(" | ")} |"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
lines
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def format_cell(column, row)
|
|
62
|
+
value = row[column]
|
|
63
|
+
|
|
64
|
+
if column == :delta
|
|
65
|
+
return "new" if row[:complexity_base].nil? && !row[:complexity_head].nil?
|
|
66
|
+
return "deleted" if row[:complexity_head].nil? && !row[:complexity_base].nil?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
value
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def title_prefix(report)
|
|
73
|
+
(report.sections.any? || report.collapsed_table) ? "##" : "#"
|
|
74
|
+
end
|
|
75
|
+
|
|
34
76
|
def format_key(key)
|
|
35
77
|
key.to_s.split("_").map(&:capitalize).join(" ")
|
|
36
78
|
end
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
module Attractor
|
|
4
4
|
module Formatters
|
|
5
5
|
class Report
|
|
6
|
-
attr_reader :title, :metadata, :columns, :rows, :footer, :group_by, :rows_key
|
|
6
|
+
attr_reader :title, :metadata, :columns, :rows, :footer, :group_by, :rows_key, :sections, :collapsed_table
|
|
7
7
|
|
|
8
|
-
def initialize(title:, columns:, rows:, metadata: {}, footer: nil, group_by: nil, rows_key: :rows)
|
|
8
|
+
def initialize(title:, columns:, rows:, metadata: {}, footer: nil, group_by: nil, rows_key: :rows, sections: [], collapsed_table: false)
|
|
9
9
|
@title = title
|
|
10
10
|
@metadata = metadata
|
|
11
11
|
@columns = columns
|
|
@@ -13,6 +13,8 @@ module Attractor
|
|
|
13
13
|
@footer = footer
|
|
14
14
|
@group_by = group_by
|
|
15
15
|
@rows_key = rows_key
|
|
16
|
+
@sections = sections
|
|
17
|
+
@collapsed_table = collapsed_table
|
|
16
18
|
end
|
|
17
19
|
end
|
|
18
20
|
end
|
|
@@ -7,18 +7,73 @@ module Attractor
|
|
|
7
7
|
module Targets
|
|
8
8
|
class Diff
|
|
9
9
|
def call(data)
|
|
10
|
+
files = data[:files]
|
|
11
|
+
|
|
10
12
|
Report.new(
|
|
11
|
-
title: "
|
|
12
|
-
metadata: {
|
|
13
|
-
total_score_base: data[:total_score_base],
|
|
14
|
-
total_score_head: data[:total_score_head],
|
|
15
|
-
trend: data[:trend]
|
|
16
|
-
},
|
|
13
|
+
title: "Attractor: #{data[:base_ref]}..#{data[:head_ref]}",
|
|
17
14
|
columns: %i[file_path complexity_base complexity_head delta churn score_head refactor_base refactor_head],
|
|
18
|
-
rows:
|
|
19
|
-
rows_key: :files
|
|
15
|
+
rows: files,
|
|
16
|
+
rows_key: :files,
|
|
17
|
+
sections: [
|
|
18
|
+
{title: "Stats", columns: %i[language score trend], rows: stats_rows(files)},
|
|
19
|
+
{title: "Trends", columns: ["", :most_improved, :largest_declines], rows: trends_rows(files)},
|
|
20
|
+
{title: "To-dos", columns: ["", :new_refactoring_candidates, :refactored], rows: todos_rows(files)}
|
|
21
|
+
],
|
|
22
|
+
collapsed_table: true
|
|
20
23
|
)
|
|
21
24
|
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def stats_rows(files)
|
|
29
|
+
rows_by_type(files).map do |type, rows|
|
|
30
|
+
total_base = rows.sum { |row| row[:score_base].to_f }
|
|
31
|
+
total_head = rows.sum { |row| row[:score_head].to_f }
|
|
32
|
+
trend = total_head - total_base
|
|
33
|
+
percent = total_base.zero? ? 0.0 : (trend / total_base * 100)
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
language: type,
|
|
37
|
+
score: "#{format("%.1f", total_head)} (from #{format("%.1f", total_base)})",
|
|
38
|
+
trend: trend.zero? ? "0.0%" : "#{trend_emoji(trend)} #{format("%+.1f", percent)}%"
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def trends_rows(files)
|
|
44
|
+
rows_by_type(files).map do |type, rows|
|
|
45
|
+
improved = rows.select { |row| row[:delta].negative? }.first(5)
|
|
46
|
+
declined = rows.select { |row| row[:delta].positive? }.first(5)
|
|
47
|
+
|
|
48
|
+
{
|
|
49
|
+
"" => type,
|
|
50
|
+
:most_improved => improved.empty? ? "none" : improved.map { |row| "#{row[:file_path]} (#{format("%.1f", row[:delta])})" }.join(", "),
|
|
51
|
+
:largest_declines => declined.empty? ? "none" : declined.map { |row| "#{row[:file_path]} (#{format("%+.1f", row[:delta])})" }.join(", ")
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def todos_rows(files)
|
|
57
|
+
rows_by_type(files).map do |type, rows|
|
|
58
|
+
flipped = rows.select { |row| row[:refactor_base] != row[:refactor_head] }
|
|
59
|
+
new_candidates = flipped.select { |row| row[:refactor_head] }
|
|
60
|
+
refactored = flipped.reject { |row| row[:refactor_head] }
|
|
61
|
+
|
|
62
|
+
{
|
|
63
|
+
"" => type,
|
|
64
|
+
:new_refactoring_candidates => new_candidates.empty? ? "none" : new_candidates.map { |row| row[:file_path] }.join(", "),
|
|
65
|
+
:refactored => refactored.empty? ? "none" : refactored.map { |row| row[:file_path] }.join(", ")
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def rows_by_type(files)
|
|
71
|
+
files.reject { |row| row[:type].nil? }.group_by { |row| row[:type] }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def trend_emoji(trend)
|
|
75
|
+
trend.positive? ? "📈" : "📉"
|
|
76
|
+
end
|
|
22
77
|
end
|
|
23
78
|
end
|
|
24
79
|
end
|
|
@@ -5,6 +5,7 @@ require "attractor/formatters/targets/console"
|
|
|
5
5
|
require "attractor/formatters/formats/json"
|
|
6
6
|
require "attractor/formatters/formats/table"
|
|
7
7
|
require "attractor/formatters/formats/csv"
|
|
8
|
+
require "attractor/formatters/formats/markdown"
|
|
8
9
|
require "attractor/formatters/format_strategy"
|
|
9
10
|
|
|
10
11
|
module Attractor
|
data/lib/attractor/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: attractor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.7.
|
|
4
|
+
version: 2.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Julian Rubisch
|
|
@@ -211,14 +211,14 @@ dependencies:
|
|
|
211
211
|
requirements:
|
|
212
212
|
- - "~>"
|
|
213
213
|
- !ruby/object:Gem::Version
|
|
214
|
-
version: 0.
|
|
214
|
+
version: 0.4.0
|
|
215
215
|
type: :development
|
|
216
216
|
prerelease: false
|
|
217
217
|
version_requirements: !ruby/object:Gem::Requirement
|
|
218
218
|
requirements:
|
|
219
219
|
- - "~>"
|
|
220
220
|
- !ruby/object:Gem::Version
|
|
221
|
-
version: 0.
|
|
221
|
+
version: 0.4.0
|
|
222
222
|
- !ruby/object:Gem::Dependency
|
|
223
223
|
name: attractor-ruby
|
|
224
224
|
requirement: !ruby/object:Gem::Requirement
|