churn_vs_complexity 1.2.0 → 1.4.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/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
|