churn_vs_complexity 1.2.0 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +19 -2
- data/TODO +11 -0
- data/bin/churn_vs_complexity +5 -0
- data/lib/churn_vs_complexity/churn.rb +7 -2
- data/lib/churn_vs_complexity/cli.rb +21 -5
- data/lib/churn_vs_complexity/complexity/eslint_calculator.rb +34 -0
- data/lib/churn_vs_complexity/complexity/flog_calculator.rb +7 -6
- data/lib/churn_vs_complexity/complexity/pmd_calculator.rb +5 -2
- data/lib/churn_vs_complexity/complexity.rb +1 -0
- data/lib/churn_vs_complexity/concurrent_calculator.rb +2 -4
- data/lib/churn_vs_complexity/config.rb +81 -17
- data/lib/churn_vs_complexity/file_selector.rb +12 -1
- data/lib/churn_vs_complexity/git_date.rb +8 -1
- data/lib/churn_vs_complexity/serializer/csv.rb +14 -0
- data/lib/churn_vs_complexity/serializer/graph.rb +24 -0
- data/lib/churn_vs_complexity/serializer/pass_through.rb +21 -0
- data/lib/churn_vs_complexity/serializer/summary.rb +27 -0
- data/lib/churn_vs_complexity/serializer/summary_hash.rb +54 -0
- data/lib/churn_vs_complexity/serializer/timetravel/quality_calculator.rb +38 -0
- data/lib/churn_vs_complexity/serializer/timetravel/stats_calculator.rb +60 -0
- data/lib/churn_vs_complexity/serializer/timetravel.rb +103 -0
- data/lib/churn_vs_complexity/serializer.rb +7 -60
- data/lib/churn_vs_complexity/timetravel/traveller.rb +66 -0
- data/lib/churn_vs_complexity/timetravel/worktree.rb +56 -0
- data/lib/churn_vs_complexity/timetravel.rb +70 -0
- data/lib/churn_vs_complexity/version.rb +1 -1
- data/lib/churn_vs_complexity.rb +2 -0
- data/package-lock.json +6 -0
- data/tmp/eslint-support/complexity-calculator.js +51 -0
- data/tmp/eslint-support/package.json +11 -0
- data/tmp/template/graph.html +1 -4
- data/tmp/template/timetravel_graph.html +100 -0
- data/tmp/test-support/javascript/complex.js +43 -0
- data/tmp/test-support/javascript/moderate.js +12 -0
- data/tmp/test-support/javascript/simple.js +5 -0
- data/tmp/test-support/javascript/typescript-example.ts +26 -0
- data/tmp/timetravel/.keep +0 -0
- metadata +24 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7484ff3a1c015738808226a78087017f6b7aff5ce42d15879023f32df5648717
|
4
|
+
data.tar.gz: ad3bdeff5ba32e9d7f414b45173d8928b1c463313557202b96aaf5fdcf059109
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a56e26296acfff22e755c414cab9ee36923e4ccd7181d67691f55feb8f99b5aa738f5d2b7645006f25c632efa346fb535d7fe04923443dedf607772ce1a21323
|
7
|
+
data.tar.gz: f406ee696facf7708e792b67ed8b463f1554365b3939deb4bde40c9fab88dfe891997f611727559a283103c5b7234f71d78d5fe108731628cb3fd8a782a73ce5
|
data/CHANGELOG.md
CHANGED
@@ -12,3 +12,14 @@
|
|
12
12
|
- Fix bug in CLI where new flags and `--since` would not be recognized
|
13
13
|
- Improve selection of observations included in the output
|
14
14
|
- Fixed calculation of churn that would never be zero
|
15
|
+
|
16
|
+
## [1.3.0] - 2024-09-26
|
17
|
+
|
18
|
+
- Add support for javascript and typescript complexity calculation using eslint
|
19
|
+
- Fixed behavior when --since or short-hand flags were not provided
|
20
|
+
|
21
|
+
## [1.4.0] - 2024-10-10
|
22
|
+
|
23
|
+
- Add timetravel mode to visualise code quality over time
|
24
|
+
- Add alpha, beta, and gamma scores to summaries
|
25
|
+
- Fixed broken Ruby complexity calculation
|
data/README.md
CHANGED
@@ -4,7 +4,7 @@ A tool to visualise code complexity in a project and help direct refactoring eff
|
|
4
4
|
|
5
5
|
Inspired by [Michael Feathers' article "Getting Empirical about Refactoring"](https://www.agileconnection.com/article/getting-empirical-about-refactoring) and the gem [turbulence](https://rubygems.org/gems/turbulence) by Chad Fowler and others.
|
6
6
|
|
7
|
-
This gem
|
7
|
+
This gem currently supports analysis of Java, Ruby, JavaScript, and TypeScript repositories, but it can easily be extended.
|
8
8
|
|
9
9
|
## Installation
|
10
10
|
|
@@ -34,17 +34,33 @@ Execute the `churn_vs_complexity` with the applicable arguments. Output in the r
|
|
34
34
|
Usage: churn_vs_complexity [options] folder
|
35
35
|
--java Check complexity of java classes
|
36
36
|
--ruby Check complexity of ruby files
|
37
|
+
--js, --ts, --javascript, --typescript
|
38
|
+
Check complexity of javascript and typescript files
|
37
39
|
--csv Format output as CSV
|
38
40
|
--graph Format output as HTML page with Churn vs Complexity graph
|
39
41
|
--summary Output summary statistics (mean and median) for churn and complexity
|
40
42
|
--excluded PATTERN Exclude file paths including this string. Can be used multiple times.
|
41
|
-
--since YYYY-MM-DD Calculate churn after this date
|
43
|
+
--since YYYY-MM-DD Normal mode: Calculate churn after this date. Timetravel mode: calculate summaries from this date
|
42
44
|
-m, --month Calculate churn for the month leading up to the most recent commit
|
43
45
|
-q, --quarter Calculate churn for the quarter leading up to the most recent commit
|
44
46
|
-y, --year Calculate churn for the year leading up to the most recent commit
|
47
|
+
--timetravel N Calculate summary for all commits at intervals of N days throughout project history or from the date specified with --since
|
48
|
+
--dry-run Echo the chosen options from the CLI
|
45
49
|
-h, --help Display help
|
50
|
+
|
51
|
+
|
46
52
|
```
|
47
53
|
|
54
|
+
Note that when using the `--timetravel` mode, the semantics of some flags are subtly different from normal mode:
|
55
|
+
|
56
|
+
* `--since YYYY-MM-DD`: Calculate summaries from this date
|
57
|
+
* `--month`, `--quarter`, `--year`: Calculate churn for the period leading up to each commit being summarised
|
58
|
+
|
59
|
+
Timetravel analysis can take many minutes for old and large repositories.
|
60
|
+
|
61
|
+
Summaries in normal mode include a gamma score, which is an unnormalised harmonic mean of churn and complexity. This allows for comparison of summaries across different projects with the same language, or over time for a single project.
|
62
|
+
|
63
|
+
Summary points in timetravel mode instead include an alpha score, which is the same harmonic mean of churn and complexity, where churn and complexity values are normalised to a 0-1 range to avoid either churn or complexity dominating the score. The summary points also include a beta score, which is the geometric mean of the normalised churn and complexity values.
|
48
64
|
## Examples
|
49
65
|
|
50
66
|
`churn_vs_complexity --ruby --csv my_ruby_project > ~/Desktop/ruby-demo.csv`
|
@@ -53,6 +69,7 @@ Usage: churn_vs_complexity [options] folder
|
|
53
69
|
|
54
70
|
`churn_vs_complexity --ruby --summary -m my_ruby_project >> ~/Desktop/monthly-report.txt`
|
55
71
|
|
72
|
+
`churn_vs_complexity --java -m --since 2019-03-01 --timetravel 30 --graph my_java_project > ~/Desktop/timetravel-after-1st-march-2019.html`
|
56
73
|
|
57
74
|
## Development
|
58
75
|
|
data/TODO
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
TODO:
|
2
|
+
|
3
|
+
- Move Timetravel calculations away from serializer
|
4
|
+
|
5
|
+
- Database, where we can prepopulate the state of every file and every commit
|
6
|
+
- Populate incrementally from each commit in log.
|
7
|
+
- Only need to care about deltas between commits, and copy everything else from previous commit.
|
8
|
+
- processed_commit table with sha and version of processing logic
|
9
|
+
- Unit tests for simpler new classes
|
10
|
+
- Integration test for Timetravel
|
11
|
+
- Candlebars on mean dots in graph
|
data/bin/churn_vs_complexity
CHANGED
@@ -8,8 +8,9 @@ module ChurnVsComplexity
|
|
8
8
|
class << self
|
9
9
|
def calculate(folder:, file:, since:)
|
10
10
|
git_dir = File.join(folder, '.git')
|
11
|
-
|
12
|
-
|
11
|
+
earliest_date = [date_of_first_commit(folder:), since].max
|
12
|
+
formatted_date = earliest_date.strftime('%Y-%m-%d')
|
13
|
+
cmd = %(git --git-dir #{git_dir} --work-tree #{folder} log --format="%H" --follow --since="#{formatted_date}" -- #{file} | wc -l)
|
13
14
|
`#{cmd}`.to_i
|
14
15
|
end
|
15
16
|
|
@@ -19,6 +20,10 @@ module ChurnVsComplexity
|
|
19
20
|
|
20
21
|
private
|
21
22
|
|
23
|
+
def date_of_first_commit(folder:)
|
24
|
+
repo(folder).log.last&.date&.to_date || Time.at(0).to_date
|
25
|
+
end
|
26
|
+
|
22
27
|
def repo(folder)
|
23
28
|
repos[folder] ||= Git.open(folder)
|
24
29
|
end
|
@@ -22,6 +22,11 @@ module ChurnVsComplexity
|
|
22
22
|
options[:language] = :ruby
|
23
23
|
end
|
24
24
|
|
25
|
+
opts.on('--js', '--ts', '--javascript', '--typescript',
|
26
|
+
'Check complexity of javascript and typescript files',) do
|
27
|
+
options[:language] = :javascript
|
28
|
+
end
|
29
|
+
|
25
30
|
opts.on('--csv', 'Format output as CSV') do
|
26
31
|
options[:serializer] = :csv
|
27
32
|
end
|
@@ -39,20 +44,27 @@ module ChurnVsComplexity
|
|
39
44
|
options[:excluded] << value
|
40
45
|
end
|
41
46
|
|
42
|
-
opts.on('--since YYYY-MM-DD',
|
47
|
+
opts.on('--since YYYY-MM-DD',
|
48
|
+
'Normal mode: Calculate churn after this date. Timetravel mode: calculate summaries from this date',) do |value|
|
43
49
|
options[:since] = value
|
44
50
|
end
|
45
51
|
|
46
52
|
opts.on('-m', '--month', 'Calculate churn for the month leading up to the most recent commit') do
|
47
|
-
options[:
|
53
|
+
options[:relative_period] = :month
|
48
54
|
end
|
49
55
|
|
50
56
|
opts.on('-q', '--quarter', 'Calculate churn for the quarter leading up to the most recent commit') do
|
51
|
-
options[:
|
57
|
+
options[:relative_period] = :quarter
|
52
58
|
end
|
53
59
|
|
54
60
|
opts.on('-y', '--year', 'Calculate churn for the year leading up to the most recent commit') do
|
55
|
-
options[:
|
61
|
+
options[:relative_period] = :year
|
62
|
+
end
|
63
|
+
|
64
|
+
opts.on('--timetravel N',
|
65
|
+
'Calculate summary for all commits at intervals of N days throughout project history or from the date specified with --since',) do |value|
|
66
|
+
options[:mode] = :timetravel
|
67
|
+
options[:jump_days] = value.to_i
|
56
68
|
end
|
57
69
|
|
58
70
|
opts.on('--dry-run', 'Echo the chosen options from the CLI') do
|
@@ -80,7 +92,11 @@ module ChurnVsComplexity
|
|
80
92
|
|
81
93
|
config.validate!
|
82
94
|
|
83
|
-
|
95
|
+
if options[:mode] == :timetravel
|
96
|
+
puts config.timetravel.go(folder:)
|
97
|
+
else
|
98
|
+
puts config.to_engine.check(folder:)
|
99
|
+
end
|
84
100
|
end
|
85
101
|
end
|
86
102
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module Complexity
|
5
|
+
module ESLintCalculator
|
6
|
+
class << self
|
7
|
+
def folder_based? = false
|
8
|
+
|
9
|
+
def calculate(files:)
|
10
|
+
dir_path = File.join(gem_root, 'tmp', 'eslint-support')
|
11
|
+
script_path = File.join(dir_path, 'complexity-calculator.js')
|
12
|
+
install_command = "npm install --prefix '#{dir_path}'"
|
13
|
+
`#{install_command}`
|
14
|
+
|
15
|
+
command = "node #{script_path} '#{files.to_json}'"
|
16
|
+
complexity = `#{command}`
|
17
|
+
|
18
|
+
raise Error, 'Failed to calculate complexity' if complexity.empty?
|
19
|
+
|
20
|
+
all = JSON.parse(complexity)
|
21
|
+
all.to_h do |abc|
|
22
|
+
[abc['file'], abc['complexity']]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def gem_root
|
29
|
+
File.expand_path('../../..', __dir__)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -5,15 +5,16 @@ require 'flog'
|
|
5
5
|
module ChurnVsComplexity
|
6
6
|
module Complexity
|
7
7
|
module FlogCalculator
|
8
|
-
CONCURRENCY = Etc.nprocessors
|
9
|
-
|
10
8
|
class << self
|
11
9
|
def folder_based? = false
|
12
10
|
|
13
|
-
def calculate(
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
def calculate(files:)
|
12
|
+
# TODO: Run this concurrently
|
13
|
+
files.to_h do |file|
|
14
|
+
flog = Flog.new
|
15
|
+
flog.flog(file)
|
16
|
+
[file, flog.total_score]
|
17
|
+
end
|
17
18
|
end
|
18
19
|
end
|
19
20
|
end
|
@@ -9,7 +9,10 @@ module ChurnVsComplexity
|
|
9
9
|
def folder_based? = true
|
10
10
|
|
11
11
|
def calculate(folder:)
|
12
|
-
|
12
|
+
cache_path = resolve_cache_path
|
13
|
+
output = `pmd check -d #{folder} -R #{resolve_ruleset_path} -f json -t #{CONCURRENCY} --cache #{cache_path}`
|
14
|
+
File.delete(cache_path)
|
15
|
+
|
13
16
|
Parser.new.parse(output)
|
14
17
|
end
|
15
18
|
|
@@ -29,7 +32,7 @@ module ChurnVsComplexity
|
|
29
32
|
end
|
30
33
|
|
31
34
|
def resolve_cache_path
|
32
|
-
File.join(gem_root, 'tmp', 'pmd-support',
|
35
|
+
File.join(gem_root, 'tmp', 'pmd-support', "pmd-cache-#{Process.pid}")
|
33
36
|
end
|
34
37
|
|
35
38
|
def gem_root
|
@@ -30,9 +30,7 @@ module ChurnVsComplexity
|
|
30
30
|
files[:explicitly_excluded].each { |file| result.delete(file) }
|
31
31
|
result
|
32
32
|
else
|
33
|
-
files[:included]
|
34
|
-
acc.merge!(@complexity.calculate(file:))
|
35
|
-
end
|
33
|
+
@complexity.calculate(files: files[:included])
|
36
34
|
end
|
37
35
|
end
|
38
36
|
|
@@ -59,7 +57,7 @@ module ChurnVsComplexity
|
|
59
57
|
def combine_results
|
60
58
|
result = {}
|
61
59
|
result[:values_by_file] = @complexity_results.keys.each_with_object({}) do |file, acc|
|
62
|
-
# File with complexity score might not have churned in queried period,
|
60
|
+
# File with complexity score might not have churned in queried period,
|
63
61
|
# set zero churn on miss
|
64
62
|
acc[file] = [@churn_results[file] || 0, @complexity_results[file]]
|
65
63
|
end
|
@@ -7,25 +7,42 @@ module ChurnVsComplexity
|
|
7
7
|
serializer:,
|
8
8
|
excluded: [],
|
9
9
|
since: nil,
|
10
|
+
relative_period: nil,
|
10
11
|
complexity_validator: ComplexityValidator,
|
11
|
-
since_validator: SinceValidator
|
12
|
+
since_validator: SinceValidator,
|
13
|
+
**options
|
12
14
|
)
|
13
15
|
@language = language
|
14
16
|
@serializer = serializer
|
15
17
|
@excluded = excluded
|
16
18
|
@since = since
|
19
|
+
@relative_period = relative_period
|
17
20
|
@complexity_validator = complexity_validator
|
18
21
|
@since_validator = since_validator
|
22
|
+
@options = options
|
19
23
|
end
|
20
24
|
|
21
25
|
def validate!
|
22
|
-
raise
|
23
|
-
raise Error, "Unsupported serializer: #{@serializer}" unless %i[none csv graph summary].include?(@serializer)
|
26
|
+
raise ValidationError, "Unsupported language: #{@language}" unless %i[java ruby javascript].include?(@language)
|
24
27
|
|
25
|
-
|
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])
|
26
32
|
@complexity_validator.validate!(@language)
|
27
33
|
end
|
28
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
|
+
|
29
46
|
def to_engine
|
30
47
|
case @language
|
31
48
|
when :java
|
@@ -34,7 +51,7 @@ module ChurnVsComplexity
|
|
34
51
|
churn:,
|
35
52
|
file_selector: FileSelector::Java.excluding(@excluded),
|
36
53
|
serializer:,
|
37
|
-
since: @since,
|
54
|
+
since: @since || @relative_period,
|
38
55
|
)
|
39
56
|
when :ruby
|
40
57
|
Engine.concurrent(
|
@@ -42,13 +59,34 @@ module ChurnVsComplexity
|
|
42
59
|
churn:,
|
43
60
|
file_selector: FileSelector::Ruby.excluding(@excluded),
|
44
61
|
serializer:,
|
45
|
-
since: @since,
|
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,
|
46
71
|
)
|
47
72
|
end
|
48
73
|
end
|
49
74
|
|
50
75
|
private
|
51
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
|
+
|
52
90
|
def churn = Churn::GitCalculator
|
53
91
|
|
54
92
|
def serializer
|
@@ -61,6 +99,8 @@ module ChurnVsComplexity
|
|
61
99
|
Serializer::Graph.new
|
62
100
|
when :summary
|
63
101
|
Serializer::Summary
|
102
|
+
when :pass_through
|
103
|
+
Serializer::PassThrough
|
64
104
|
end
|
65
105
|
end
|
66
106
|
|
@@ -73,21 +113,45 @@ module ChurnVsComplexity
|
|
73
113
|
end
|
74
114
|
end
|
75
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
|
+
|
76
139
|
module SinceValidator
|
77
|
-
def self.validate!(since)
|
140
|
+
def self.validate!(since:, relative_period:, mode:)
|
78
141
|
# since can be nil, a date string or a keyword (:month, :quarter, :year)
|
79
142
|
return if since.nil?
|
80
143
|
|
81
|
-
|
82
|
-
raise
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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"
|
91
155
|
end
|
92
156
|
end
|
93
157
|
end
|
@@ -10,9 +10,10 @@ module ChurnVsComplexity
|
|
10
10
|
end
|
11
11
|
|
12
12
|
class Excluding
|
13
|
-
def initialize(extensions, excluded)
|
13
|
+
def initialize(extensions, excluded, convert_to_absolute_path = false)
|
14
14
|
@extensions = extensions
|
15
15
|
@excluded = excluded
|
16
|
+
@convert_to_absolute_path = convert_to_absolute_path
|
16
17
|
end
|
17
18
|
|
18
19
|
def select_files(folder)
|
@@ -25,6 +26,10 @@ module ChurnVsComplexity
|
|
25
26
|
were_included << f
|
26
27
|
end
|
27
28
|
end
|
29
|
+
if @convert_to_absolute_path
|
30
|
+
were_excluded.map! { |f| File.absolute_path(f) }
|
31
|
+
were_included.map! { |f| File.absolute_path(f) }
|
32
|
+
end
|
28
33
|
{ explicitly_excluded: were_excluded, included: were_included }
|
29
34
|
end
|
30
35
|
|
@@ -50,5 +55,11 @@ module ChurnVsComplexity
|
|
50
55
|
Excluding.new(['.rb'], excluded)
|
51
56
|
end
|
52
57
|
end
|
58
|
+
|
59
|
+
module JavaScript
|
60
|
+
def self.excluding(excluded)
|
61
|
+
Excluding.new(['.js', '.jsx', '.ts', '.tsx'], excluded, true)
|
62
|
+
end
|
63
|
+
end
|
53
64
|
end
|
54
65
|
end
|
@@ -2,6 +2,13 @@
|
|
2
2
|
|
3
3
|
module ChurnVsComplexity
|
4
4
|
module GitDate
|
5
|
+
def self.select_dates_with_at_least_interval(dates, interval)
|
6
|
+
ds = dates.sort
|
7
|
+
ds.each_with_object([]) do |date, acc|
|
8
|
+
acc << date if acc.empty? || date - acc.last >= interval
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
5
12
|
def self.git_period(cli_arg_since, latest_commit_date)
|
6
13
|
latest_commit_date = latest_commit_date.to_date
|
7
14
|
if cli_arg_since.nil?
|
@@ -22,7 +29,7 @@ module ChurnVsComplexity
|
|
22
29
|
@end_date = end_date
|
23
30
|
end
|
24
31
|
|
25
|
-
def effective_start_date = Time.at(0)
|
32
|
+
def effective_start_date = Time.at(0).to_date
|
26
33
|
|
27
34
|
def requested_start_date = nil
|
28
35
|
end
|
@@ -0,0 +1,14 @@
|
|
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
|
@@ -0,0 +1,24 @@
|
|
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
|
@@ -0,0 +1,21 @@
|
|
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
|
@@ -0,0 +1,27 @@
|
|
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
|
@@ -0,0 +1,54 @@
|
|
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
|