churn_vs_complexity 1.4.0 → 1.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +29 -14
- data/README.md +10 -4
- data/lib/churn_vs_complexity/churn.rb +9 -1
- data/lib/churn_vs_complexity/cli/main.rb +46 -0
- data/lib/churn_vs_complexity/cli/parser.rb +91 -0
- data/lib/churn_vs_complexity/cli.rb +11 -94
- data/lib/churn_vs_complexity/complexity/eslint_calculator.rb +6 -0
- data/lib/churn_vs_complexity/complexity/pmd/files_calculator.rb +26 -0
- data/lib/churn_vs_complexity/complexity/pmd/folder_calculator.rb +20 -0
- data/lib/churn_vs_complexity/complexity/{pmd_calculator.rb → pmd.rb} +14 -21
- data/lib/churn_vs_complexity/complexity.rb +1 -1
- data/lib/churn_vs_complexity/complexity_validator.rb +14 -0
- data/lib/churn_vs_complexity/concurrent_calculator.rb +5 -3
- data/lib/churn_vs_complexity/delta/checker.rb +54 -0
- data/lib/churn_vs_complexity/delta/commit_hydrator.rb +22 -0
- data/lib/churn_vs_complexity/delta/complexity_annotator.rb +30 -0
- data/lib/churn_vs_complexity/delta/config.rb +50 -0
- data/lib/churn_vs_complexity/delta/factory.rb +22 -0
- data/lib/churn_vs_complexity/delta/multi_checker.rb +48 -0
- data/lib/churn_vs_complexity/delta/serializer.rb +69 -0
- data/lib/churn_vs_complexity/delta.rb +52 -0
- data/lib/churn_vs_complexity/engine.rb +1 -1
- data/lib/churn_vs_complexity/file_selector.rb +47 -4
- data/lib/churn_vs_complexity/git_strategy.rb +62 -0
- data/lib/churn_vs_complexity/language_validator.rb +9 -0
- data/lib/churn_vs_complexity/normal/config.rb +85 -0
- data/lib/churn_vs_complexity/normal/serializer/csv.rb +16 -0
- data/lib/churn_vs_complexity/normal/serializer/graph.rb +26 -0
- data/lib/churn_vs_complexity/normal/serializer/pass_through.rb +23 -0
- data/lib/churn_vs_complexity/normal/serializer/summary.rb +29 -0
- data/lib/churn_vs_complexity/normal/serializer/summary_hash.rb +56 -0
- data/lib/churn_vs_complexity/normal/serializer.rb +29 -0
- data/lib/churn_vs_complexity/normal.rb +45 -0
- data/lib/churn_vs_complexity/timetravel/config.rb +75 -0
- data/lib/churn_vs_complexity/timetravel/factory.rb +12 -0
- data/lib/churn_vs_complexity/{serializer/timetravel → timetravel/serializer}/quality_calculator.rb +2 -2
- data/lib/churn_vs_complexity/{serializer/timetravel → timetravel/serializer}/stats_calculator.rb +2 -2
- data/lib/churn_vs_complexity/{serializer/timetravel.rb → timetravel/serializer.rb} +6 -6
- data/lib/churn_vs_complexity/timetravel/traveller.rb +5 -11
- data/lib/churn_vs_complexity/timetravel/worktree.rb +30 -14
- data/lib/churn_vs_complexity/timetravel.rb +36 -39
- data/lib/churn_vs_complexity/version.rb +1 -1
- data/lib/churn_vs_complexity.rb +23 -7
- data/tmp/test-support/delta/ruby-summary.txt +50 -0
- data/tmp/test-support/delta/ruby.csv +12 -0
- metadata +38 -20
- data/.travis.yml +0 -7
- data/lib/churn_vs_complexity/config.rb +0 -159
- data/lib/churn_vs_complexity/serializer/csv.rb +0 -14
- data/lib/churn_vs_complexity/serializer/graph.rb +0 -24
- data/lib/churn_vs_complexity/serializer/pass_through.rb +0 -21
- data/lib/churn_vs_complexity/serializer/summary.rb +0 -27
- data/lib/churn_vs_complexity/serializer/summary_hash.rb +0 -54
- data/lib/churn_vs_complexity/serializer.rb +0 -26
@@ -1,159 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ChurnVsComplexity
|
4
|
-
class Config
|
5
|
-
def initialize(
|
6
|
-
language:,
|
7
|
-
serializer:,
|
8
|
-
excluded: [],
|
9
|
-
since: nil,
|
10
|
-
relative_period: nil,
|
11
|
-
complexity_validator: ComplexityValidator,
|
12
|
-
since_validator: SinceValidator,
|
13
|
-
**options
|
14
|
-
)
|
15
|
-
@language = language
|
16
|
-
@serializer = serializer
|
17
|
-
@excluded = excluded
|
18
|
-
@since = since
|
19
|
-
@relative_period = relative_period
|
20
|
-
@complexity_validator = complexity_validator
|
21
|
-
@since_validator = since_validator
|
22
|
-
@options = options
|
23
|
-
end
|
24
|
-
|
25
|
-
def validate!
|
26
|
-
raise ValidationError, "Unsupported language: #{@language}" unless %i[java ruby javascript].include?(@language)
|
27
|
-
|
28
|
-
SerializerValidator.validate!(serializer: @serializer, mode: @options[:mode])
|
29
|
-
|
30
|
-
@since_validator.validate!(since: @since, relative_period: @relative_period, mode: @options[:mode])
|
31
|
-
RelativePeriodValidator.validate!(relative_period: @relative_period, mode: @options[:mode])
|
32
|
-
@complexity_validator.validate!(@language)
|
33
|
-
end
|
34
|
-
|
35
|
-
def timetravel
|
36
|
-
engine = timetravel_engine_config.to_engine
|
37
|
-
Timetravel::Traveller.new(
|
38
|
-
since: @since,
|
39
|
-
relative_period: @relative_period,
|
40
|
-
engine:,
|
41
|
-
jump_days: @options[:jump_days],
|
42
|
-
serializer: @serializer,
|
43
|
-
)
|
44
|
-
end
|
45
|
-
|
46
|
-
def to_engine
|
47
|
-
case @language
|
48
|
-
when :java
|
49
|
-
Engine.concurrent(
|
50
|
-
complexity: Complexity::PMDCalculator,
|
51
|
-
churn:,
|
52
|
-
file_selector: FileSelector::Java.excluding(@excluded),
|
53
|
-
serializer:,
|
54
|
-
since: @since || @relative_period,
|
55
|
-
)
|
56
|
-
when :ruby
|
57
|
-
Engine.concurrent(
|
58
|
-
complexity: Complexity::FlogCalculator,
|
59
|
-
churn:,
|
60
|
-
file_selector: FileSelector::Ruby.excluding(@excluded),
|
61
|
-
serializer:,
|
62
|
-
since: @since || @relative_period,
|
63
|
-
)
|
64
|
-
when :javascript
|
65
|
-
Engine.concurrent(
|
66
|
-
complexity: Complexity::ESLintCalculator,
|
67
|
-
churn:,
|
68
|
-
file_selector: FileSelector::JavaScript.excluding(@excluded),
|
69
|
-
serializer:,
|
70
|
-
since: @since || @relative_period,
|
71
|
-
)
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
private
|
76
|
-
|
77
|
-
def timetravel_engine_config
|
78
|
-
Config.new(
|
79
|
-
language: @language,
|
80
|
-
serializer: :pass_through,
|
81
|
-
excluded: @excluded,
|
82
|
-
since: nil, # since has a different meaning in timetravel mode
|
83
|
-
relative_period: @relative_period,
|
84
|
-
complexity_validator: @complexity_validator,
|
85
|
-
since_validator: @since_validator,
|
86
|
-
**@options,
|
87
|
-
)
|
88
|
-
end
|
89
|
-
|
90
|
-
def churn = Churn::GitCalculator
|
91
|
-
|
92
|
-
def serializer
|
93
|
-
case @serializer
|
94
|
-
when :none
|
95
|
-
Serializer::None
|
96
|
-
when :csv
|
97
|
-
Serializer::CSV
|
98
|
-
when :graph
|
99
|
-
Serializer::Graph.new
|
100
|
-
when :summary
|
101
|
-
Serializer::Summary
|
102
|
-
when :pass_through
|
103
|
-
Serializer::PassThrough
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
module ComplexityValidator
|
108
|
-
def self.validate!(language)
|
109
|
-
case language
|
110
|
-
when :java
|
111
|
-
Complexity::PMDCalculator.check_dependencies!
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
# TODO: unit test
|
117
|
-
module SerializerValidator
|
118
|
-
def self.validate!(serializer:, mode:)
|
119
|
-
raise ValidationError, "Unsupported serializer: #{serializer}" \
|
120
|
-
unless %i[none csv graph summary].include?(serializer)
|
121
|
-
raise ValidationError, 'Does not support --summary in --timetravel mode' \
|
122
|
-
if serializer == :summary && mode == :timetravel
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
# TODO: unit test
|
127
|
-
module RelativePeriodValidator
|
128
|
-
def self.validate!(relative_period:, mode:)
|
129
|
-
if mode == :timetravel && relative_period.nil?
|
130
|
-
raise ValidationError,
|
131
|
-
'Relative period is required in timetravel mode'
|
132
|
-
end
|
133
|
-
return if relative_period.nil? || %i[month quarter year].include?(relative_period)
|
134
|
-
|
135
|
-
raise ValidationError, "Invalid relative period #{relative_period}"
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
|
-
module SinceValidator
|
140
|
-
def self.validate!(since:, relative_period:, mode:)
|
141
|
-
# since can be nil, a date string or a keyword (:month, :quarter, :year)
|
142
|
-
return if since.nil?
|
143
|
-
|
144
|
-
unless mode == :timetravel || since.nil? || relative_period.nil?
|
145
|
-
raise ValidationError,
|
146
|
-
'--since and relative period (--month, --quarter, --year) can only be used together in --timetravel mode'
|
147
|
-
end
|
148
|
-
|
149
|
-
raise ValidationError, "Invalid since value #{since}" unless since.is_a?(String)
|
150
|
-
|
151
|
-
begin
|
152
|
-
Date.strptime(since, '%Y-%m-%d')
|
153
|
-
rescue Date::Error
|
154
|
-
raise ValidationError, "Invalid date #{since}, please use correct format, YYYY-MM-DD"
|
155
|
-
end
|
156
|
-
end
|
157
|
-
end
|
158
|
-
end
|
159
|
-
end
|
@@ -1,14 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ChurnVsComplexity
|
4
|
-
module Serializer
|
5
|
-
module CSV
|
6
|
-
def self.serialize(result)
|
7
|
-
values_by_file = result[:values_by_file]
|
8
|
-
values_by_file.map do |file, values|
|
9
|
-
"#{file},#{values[0]},#{values[1]}\n"
|
10
|
-
end.join
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
@@ -1,24 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ChurnVsComplexity
|
4
|
-
module Serializer
|
5
|
-
class Graph
|
6
|
-
def initialize(template: Graph.load_template_file)
|
7
|
-
@template = template
|
8
|
-
end
|
9
|
-
|
10
|
-
def serialize(result)
|
11
|
-
data = result[:values_by_file].map do |file, values|
|
12
|
-
"{ file_path: '#{file}', churn: #{values[0]}, complexity: #{values[1]} }"
|
13
|
-
end.join(",\n") + "\n"
|
14
|
-
title = Serializer.title(result)
|
15
|
-
@template.gsub("// INSERT DATA\n", data).gsub('INSERT TITLE', title)
|
16
|
-
end
|
17
|
-
|
18
|
-
def self.load_template_file
|
19
|
-
file_path = File.expand_path('../../../tmp/template/graph.html', __dir__)
|
20
|
-
File.read(file_path)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
@@ -1,21 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ChurnVsComplexity
|
4
|
-
module Serializer
|
5
|
-
module PassThrough
|
6
|
-
class << self
|
7
|
-
def serialize(result)
|
8
|
-
values_by_file = result[:values_by_file]
|
9
|
-
end_date = result[:git_period].end_date
|
10
|
-
values = values_by_file.map do |_, values|
|
11
|
-
[values[0].to_f, values[1].to_f]
|
12
|
-
end
|
13
|
-
{
|
14
|
-
end_date:,
|
15
|
-
values:,
|
16
|
-
}
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
@@ -1,27 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ChurnVsComplexity
|
4
|
-
module Serializer
|
5
|
-
module Summary
|
6
|
-
def self.serialize(result)
|
7
|
-
values_by_file = result[:values_by_file]
|
8
|
-
summary = SummaryHash.serialize(result)
|
9
|
-
|
10
|
-
<<~SUMMARY
|
11
|
-
#{Serializer.title(result)}
|
12
|
-
|
13
|
-
Number of observations: #{values_by_file.size}
|
14
|
-
|
15
|
-
Churn:
|
16
|
-
Mean #{summary[:mean_churn]}, Median #{summary[:median_churn]}
|
17
|
-
|
18
|
-
Complexity:
|
19
|
-
Mean #{summary[:mean_complexity]}, Median #{summary[:median_complexity]}
|
20
|
-
|
21
|
-
Gamma score:
|
22
|
-
Mean #{summary[:mean_gamma_score]}, Median #{summary[:median_gamma_score]}
|
23
|
-
SUMMARY
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
@@ -1,54 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ChurnVsComplexity
|
4
|
-
module Serializer
|
5
|
-
module SummaryHash
|
6
|
-
class << self
|
7
|
-
def serialize(result)
|
8
|
-
values_by_file = result[:values_by_file]
|
9
|
-
churn_values = values_by_file.map { |_, values| values[0].to_f }
|
10
|
-
complexity_values = values_by_file.map { |_, values| values[1].to_f }
|
11
|
-
|
12
|
-
mean_churn = churn_values.sum / churn_values.size
|
13
|
-
median_churn = churn_values.sort[churn_values.size / 2]
|
14
|
-
mean_complexity = complexity_values.sum / complexity_values.size
|
15
|
-
median_complexity = complexity_values.sort[complexity_values.size / 2]
|
16
|
-
|
17
|
-
max_churn = churn_values.max
|
18
|
-
min_churn = churn_values.min
|
19
|
-
max_complexity = complexity_values.max
|
20
|
-
min_complexity = complexity_values.min
|
21
|
-
|
22
|
-
epsilon = 0.0001
|
23
|
-
gamma_score = values_by_file.map do |_, values|
|
24
|
-
# unnormalised harmonic mean of churn and complexity,
|
25
|
-
# since the summary needs to be comparable over time
|
26
|
-
churn = values[0].to_f + epsilon
|
27
|
-
complexity = values[1].to_f + epsilon
|
28
|
-
|
29
|
-
(2 * churn * complexity) / (churn + complexity)
|
30
|
-
end
|
31
|
-
|
32
|
-
mean_gamma_score = gamma_score.sum / gamma_score.size
|
33
|
-
median_gamma_score = gamma_score.sort[gamma_score.size / 2]
|
34
|
-
|
35
|
-
end_date = result[:git_period].end_date
|
36
|
-
|
37
|
-
{
|
38
|
-
mean_churn:,
|
39
|
-
median_churn:,
|
40
|
-
max_churn:,
|
41
|
-
min_churn:,
|
42
|
-
mean_complexity:,
|
43
|
-
median_complexity:,
|
44
|
-
max_complexity:,
|
45
|
-
min_complexity:,
|
46
|
-
mean_gamma_score:,
|
47
|
-
median_gamma_score:,
|
48
|
-
end_date:,
|
49
|
-
}
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
@@ -1,26 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'serializer/timetravel'
|
4
|
-
require_relative 'serializer/summary_hash'
|
5
|
-
require_relative 'serializer/summary'
|
6
|
-
require_relative 'serializer/csv'
|
7
|
-
require_relative 'serializer/graph'
|
8
|
-
require_relative 'serializer/pass_through'
|
9
|
-
|
10
|
-
module ChurnVsComplexity
|
11
|
-
module Serializer
|
12
|
-
def self.title(result)
|
13
|
-
requested_start_date = result[:git_period].requested_start_date
|
14
|
-
end_date = result[:git_period].end_date
|
15
|
-
if requested_start_date.nil?
|
16
|
-
"Churn until #{end_date.strftime('%Y-%m-%d')} vs complexity"
|
17
|
-
else
|
18
|
-
"Churn between #{requested_start_date.strftime('%Y-%m-%d')} and #{end_date.strftime('%Y-%m-%d')} vs complexity"
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
module None
|
23
|
-
def self.serialize(result) = result
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|