churn_vs_complexity 1.4.0 → 1.5.2
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/.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
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module Delta
|
5
|
+
class Config
|
6
|
+
def initialize(
|
7
|
+
language:,
|
8
|
+
serializer:,
|
9
|
+
commits:,
|
10
|
+
excluded: [],
|
11
|
+
complexity_validator: ComplexityValidator,
|
12
|
+
factory: Factory,
|
13
|
+
**_options
|
14
|
+
)
|
15
|
+
@language = language
|
16
|
+
@serializer = serializer
|
17
|
+
@excluded = excluded
|
18
|
+
@commits = commits
|
19
|
+
@factory = factory
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate!
|
23
|
+
validate_commits!
|
24
|
+
LanguageValidator.validate!(@language)
|
25
|
+
SerializerValidator.validate!(serializer: @serializer)
|
26
|
+
@factory.complexity_validator.validate!(@language)
|
27
|
+
end
|
28
|
+
|
29
|
+
def checker
|
30
|
+
MultiChecker.new(serializer:, excluded: @excluded, factory: @factory, commits: @commits,
|
31
|
+
language: @language,)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def validate_commits!
|
37
|
+
@commits.each { |commit| validate_commit!(commit) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def validate_commit!(commit)
|
41
|
+
return if commit == 'HEAD' || commit.match?(/\A[0-9a-f]{40}\z/i) || commit.match?(/\A[0-9a-f]{8}\z/i)
|
42
|
+
|
43
|
+
raise ValidationError,
|
44
|
+
"Invalid commit: #{commit}. It must be a valid 40-character SHA-1 hash or an 8-character shortened form."
|
45
|
+
end
|
46
|
+
|
47
|
+
def serializer = Serializer.resolve(@serializer)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module Delta
|
5
|
+
module Factory
|
6
|
+
def self.complexity_validator = ComplexityValidator
|
7
|
+
def self.git_strategy(folder:) = GitStrategy.new(folder:)
|
8
|
+
|
9
|
+
def self.worktree(root_folder:, git_strategy:, data_isolation_id:)
|
10
|
+
Timetravel::Worktree.new(
|
11
|
+
root_folder:,
|
12
|
+
git_strategy:,
|
13
|
+
number: data_isolation_id,
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.engine(cache_components:, language:, excluded:, files:)
|
18
|
+
Delta.engine(cache_components:, language:, excluded:, files:)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module Delta
|
5
|
+
CONCURRENCY = Etc.nprocessors * 2
|
6
|
+
|
7
|
+
class MultiChecker
|
8
|
+
def initialize(serializer:, factory:, commits:, language:, excluded:)
|
9
|
+
@serializer = serializer
|
10
|
+
@excluded = excluded
|
11
|
+
@factory = factory
|
12
|
+
@commits = commits
|
13
|
+
@language = language
|
14
|
+
end
|
15
|
+
|
16
|
+
def check(folder:)
|
17
|
+
indexed_commits = @commits.map.with_index do |commit, index|
|
18
|
+
[commit, index]
|
19
|
+
end
|
20
|
+
|
21
|
+
results = []
|
22
|
+
|
23
|
+
concurrency = [CONCURRENCY, indexed_commits.size].min
|
24
|
+
|
25
|
+
concurrency.times.map do |ci|
|
26
|
+
Thread.new do
|
27
|
+
loop do
|
28
|
+
commit, index = indexed_commits.shift
|
29
|
+
break if commit.nil?
|
30
|
+
|
31
|
+
result = check_commit(commit:, data_isolation_id: ci, folder:)
|
32
|
+
results[index] = result
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end.each(&:join)
|
36
|
+
|
37
|
+
@serializer.serialize(results)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def check_commit(commit:, data_isolation_id:, folder:)
|
43
|
+
Checker.new(serializer: Serializer::PassThrough, factory: @factory, commit:, language: @language,
|
44
|
+
excluded: @excluded, data_isolation_id:,).check(folder:)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module Delta
|
5
|
+
module Serializer
|
6
|
+
def self.resolve(serializer)
|
7
|
+
case serializer
|
8
|
+
when :none
|
9
|
+
Normal::Serializer::None
|
10
|
+
when :csv
|
11
|
+
CSV
|
12
|
+
when :summary
|
13
|
+
Summary
|
14
|
+
when :pass_through
|
15
|
+
PassThrough
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module CSV
|
20
|
+
def self.serialize(result)
|
21
|
+
results = result.is_a?(Array) ? result : [result]
|
22
|
+
changes = results.flat_map { |r| r[:changes].each { |c| c[:commit] = r[:commit] } }
|
23
|
+
rows = ['Commit, Relative Path, Type of Change, Complexity']
|
24
|
+
changes.each do |change|
|
25
|
+
rows << "#{change[:commit]}, #{change[:path]}, #{change[:type]}, #{change[:complexity]}"
|
26
|
+
end
|
27
|
+
rows.join("\n")
|
28
|
+
end
|
29
|
+
|
30
|
+
def has_commit_summary? = false
|
31
|
+
end
|
32
|
+
|
33
|
+
module Summary
|
34
|
+
class << self
|
35
|
+
def serialize(result)
|
36
|
+
results = result.is_a?(Array) ? result : [result]
|
37
|
+
results.map { |r| serialize_single(r) }.join("\n\n\n\n")
|
38
|
+
end
|
39
|
+
|
40
|
+
def has_commit_summary? = true
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def serialize_single(result)
|
45
|
+
changes = result[:changes]
|
46
|
+
|
47
|
+
commit_text = "Commit: #{result[:commit]}\nParent: #{result[:parent]}\nNext: #{result[:next_commit]}"
|
48
|
+
change_text = changes.empty? ? '(No changes)' : describe(changes)
|
49
|
+
|
50
|
+
"#{commit_text}\n\n\n#{change_text}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def describe(changes)
|
54
|
+
changes.map do |change|
|
55
|
+
a = "File, relative path: #{change[:path]}\nType of change: #{change[:type]}"
|
56
|
+
b = "\nComplexity: #{change[:complexity]}" unless change[:complexity].nil?
|
57
|
+
"#{a}#{b}"
|
58
|
+
end.join("\n\n")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
module PassThrough
|
64
|
+
extend Normal::Serializer::None
|
65
|
+
def self.has_commit_summary? = true
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'delta/config'
|
4
|
+
require_relative 'delta/checker'
|
5
|
+
require_relative 'delta/serializer'
|
6
|
+
require_relative 'delta/factory'
|
7
|
+
require_relative 'delta/complexity_annotator'
|
8
|
+
require_relative 'delta/multi_checker'
|
9
|
+
require_relative 'delta/commit_hydrator'
|
10
|
+
|
11
|
+
module ChurnVsComplexity
|
12
|
+
module Delta
|
13
|
+
module SerializerValidator
|
14
|
+
def self.validate!(serializer:); end
|
15
|
+
end
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def engine(cache_components:, language:, excluded:, files:)
|
19
|
+
file_selector = file_selector(language:, included: files, excluded:)
|
20
|
+
Engine.concurrent(
|
21
|
+
since: nil, complexity: complexity(language, cache_components), churn: Churn::Disabled,
|
22
|
+
serializer: Normal::Serializer::None,
|
23
|
+
file_selector:,
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def file_selector(language:, included:, excluded:)
|
30
|
+
case language
|
31
|
+
when :java
|
32
|
+
FileSelector::Java.predefined(included:, excluded:)
|
33
|
+
when :ruby
|
34
|
+
FileSelector::Ruby.predefined(included:, excluded:)
|
35
|
+
when :javascript
|
36
|
+
FileSelector::JavaScript.predefined(included:, excluded:)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def complexity(language, cache_components)
|
41
|
+
case language
|
42
|
+
when :java
|
43
|
+
Complexity::PMD::FilesCalculator.new(cache_components:)
|
44
|
+
when :ruby
|
45
|
+
Complexity::FlogCalculator
|
46
|
+
when :javascript
|
47
|
+
Complexity::ESLintCalculator
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -15,7 +15,7 @@ module ChurnVsComplexity
|
|
15
15
|
@serializer.serialize(result)
|
16
16
|
end
|
17
17
|
|
18
|
-
def self.concurrent(since:, complexity:, churn:, serializer: Serializer::None, file_selector: FileSelector::Any)
|
18
|
+
def self.concurrent(since:, complexity:, churn:, serializer: Normal::Serializer::None, file_selector: FileSelector::Any)
|
19
19
|
Engine.new(since:, file_selector:, serializer:, calculator: ConcurrentCalculator.new(complexity:, churn:))
|
20
20
|
end
|
21
21
|
end
|
@@ -2,6 +2,19 @@
|
|
2
2
|
|
3
3
|
module ChurnVsComplexity
|
4
4
|
module FileSelector
|
5
|
+
def self.extensions(language)
|
6
|
+
case language
|
7
|
+
when :java
|
8
|
+
['.java']
|
9
|
+
when :ruby
|
10
|
+
['.rb']
|
11
|
+
when :javascript
|
12
|
+
['.js', '.jsx', '.ts', '.tsx']
|
13
|
+
else
|
14
|
+
raise Error, "Unsupported language: #{language}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
5
18
|
module Any
|
6
19
|
def self.select_files(folder)
|
7
20
|
included = Dir.glob("#{folder}/**/*").select { |f| File.file?(f) }
|
@@ -19,7 +32,7 @@ module ChurnVsComplexity
|
|
19
32
|
def select_files(folder)
|
20
33
|
were_excluded = []
|
21
34
|
were_included = []
|
22
|
-
|
35
|
+
candidates(folder).each do |f|
|
23
36
|
if has_excluded_pattern?(f)
|
24
37
|
were_excluded << f
|
25
38
|
elsif has_correct_extension?(f) && File.file?(f)
|
@@ -33,6 +46,12 @@ module ChurnVsComplexity
|
|
33
46
|
{ explicitly_excluded: were_excluded, included: were_included }
|
34
47
|
end
|
35
48
|
|
49
|
+
protected
|
50
|
+
|
51
|
+
def candidates(folder)
|
52
|
+
Dir.glob("#{folder}/**/*")
|
53
|
+
end
|
54
|
+
|
36
55
|
private
|
37
56
|
|
38
57
|
def has_correct_extension?(file_path)
|
@@ -44,21 +63,45 @@ module ChurnVsComplexity
|
|
44
63
|
end
|
45
64
|
end
|
46
65
|
|
66
|
+
class Predefined < Excluding
|
67
|
+
def initialize(included:, extensions:, excluded:, convert_to_absolute_path: false)
|
68
|
+
super(extensions, excluded, convert_to_absolute_path)
|
69
|
+
@included = included
|
70
|
+
end
|
71
|
+
|
72
|
+
protected
|
73
|
+
|
74
|
+
def candidates(*) = @included
|
75
|
+
end
|
76
|
+
|
47
77
|
module Java
|
48
78
|
def self.excluding(excluded)
|
49
|
-
Excluding.new(
|
79
|
+
Excluding.new(FileSelector.extensions(:java), excluded)
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.predefined(included:, excluded:)
|
83
|
+
Predefined.new(included:, extensions: FileSelector.extensions(:java), excluded:)
|
50
84
|
end
|
51
85
|
end
|
52
86
|
|
53
87
|
module Ruby
|
54
88
|
def self.excluding(excluded)
|
55
|
-
Excluding.new(
|
89
|
+
Excluding.new(FileSelector.extensions(:ruby), excluded)
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.predefined(included:, excluded:)
|
93
|
+
Predefined.new(included:, extensions: FileSelector.extensions(:ruby), excluded:)
|
56
94
|
end
|
57
95
|
end
|
58
96
|
|
59
97
|
module JavaScript
|
60
98
|
def self.excluding(excluded)
|
61
|
-
Excluding.new(
|
99
|
+
Excluding.new(FileSelector.extensions(:javascript), excluded, true)
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.predefined(included:, excluded:)
|
103
|
+
Predefined.new(included:, extensions: FileSelector.extensions(:javascript), excluded:,
|
104
|
+
convert_to_absolute_path: true,)
|
62
105
|
end
|
63
106
|
end
|
64
107
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
class GitStrategy
|
5
|
+
def initialize(folder:)
|
6
|
+
@repo = Git.open(folder)
|
7
|
+
@folder = folder
|
8
|
+
end
|
9
|
+
|
10
|
+
def valid_commit?(commit:)
|
11
|
+
@repo.object(commit)
|
12
|
+
true
|
13
|
+
rescue Git::GitExecuteError
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
def object(commit)
|
18
|
+
commit.is_a?(Git::Object::Commit) ? commit : @repo.object(commit)
|
19
|
+
end
|
20
|
+
|
21
|
+
def surrounding(commit:)
|
22
|
+
current = object(commit)
|
23
|
+
is_head = current.sha == @repo.object('HEAD').sha
|
24
|
+
next_commit = is_head ? nil : @repo.log(100_000).find { |c| c.parents.map(&:sha).include?(current.sha) }
|
25
|
+
[current.parent&.sha, next_commit&.sha]
|
26
|
+
end
|
27
|
+
|
28
|
+
def changes(commit:)
|
29
|
+
commit_object = @repo.object(commit)
|
30
|
+
base = commit_object.parent
|
31
|
+
commit_object.diff(base).map do |change|
|
32
|
+
{ path: change.path, type: change.type.to_sym }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def checkout_in_worktree(worktree_folder, sha)
|
37
|
+
command = "(cd #{worktree_folder} && git checkout #{sha}) > /dev/null 2>&1"
|
38
|
+
`#{command}`
|
39
|
+
end
|
40
|
+
|
41
|
+
def resolve_commits_with_interval(git_period:, jump_days:)
|
42
|
+
candidates = @repo.log(1_000_000).since(git_period.effective_start_date).until(git_period.end_date).to_a
|
43
|
+
|
44
|
+
commits_by_date = candidates.filter { |c| c.date.to_date >= git_period.effective_start_date }
|
45
|
+
.group_by { |c| c.date.to_date }
|
46
|
+
|
47
|
+
found_dates = GitDate.select_dates_with_at_least_interval(commits_by_date.keys, jump_days)
|
48
|
+
|
49
|
+
found_dates.map { |date| commits_by_date[date].max_by(&:date) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_worktree(wt_folder)
|
53
|
+
command = "(cd #{@folder} && git worktree add -f #{wt_folder}) > /dev/null 2>&1"
|
54
|
+
`#{command}`
|
55
|
+
end
|
56
|
+
|
57
|
+
def remove_worktree(worktree_folder)
|
58
|
+
command = "(cd #{worktree_folder} && git worktree remove -f #{worktree_folder}) > /dev/null 2>&1"
|
59
|
+
`#{command}`
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module Normal
|
5
|
+
class Config
|
6
|
+
def initialize(
|
7
|
+
language:,
|
8
|
+
serializer:,
|
9
|
+
excluded: [],
|
10
|
+
since: nil,
|
11
|
+
relative_period: nil,
|
12
|
+
complexity_validator: ComplexityValidator,
|
13
|
+
since_validator: SinceValidator,
|
14
|
+
**options
|
15
|
+
)
|
16
|
+
@language = language
|
17
|
+
@serializer = serializer
|
18
|
+
@excluded = excluded
|
19
|
+
@since = since
|
20
|
+
@relative_period = relative_period
|
21
|
+
@complexity_validator = complexity_validator
|
22
|
+
@since_validator = since_validator
|
23
|
+
@options = options
|
24
|
+
end
|
25
|
+
|
26
|
+
def validate!
|
27
|
+
LanguageValidator.validate!(@language)
|
28
|
+
|
29
|
+
SerializerValidator.validate!(serializer: @serializer)
|
30
|
+
|
31
|
+
@since_validator.validate!(since: @since, relative_period: @relative_period)
|
32
|
+
RelativePeriodValidator.validate!(relative_period: @relative_period)
|
33
|
+
@complexity_validator.validate!(@language)
|
34
|
+
end
|
35
|
+
|
36
|
+
def checker
|
37
|
+
case @language
|
38
|
+
when :java
|
39
|
+
Engine.concurrent(
|
40
|
+
complexity: Complexity::PMD::FolderCalculator,
|
41
|
+
churn:,
|
42
|
+
file_selector: FileSelector::Java.excluding(@excluded),
|
43
|
+
serializer:,
|
44
|
+
since: @since || @relative_period,
|
45
|
+
)
|
46
|
+
when :ruby
|
47
|
+
Engine.concurrent(
|
48
|
+
complexity: Complexity::FlogCalculator,
|
49
|
+
churn:,
|
50
|
+
file_selector: FileSelector::Ruby.excluding(@excluded),
|
51
|
+
serializer:,
|
52
|
+
since: @since || @relative_period,
|
53
|
+
)
|
54
|
+
when :javascript
|
55
|
+
Engine.concurrent(
|
56
|
+
complexity: Complexity::ESLintCalculator,
|
57
|
+
churn:,
|
58
|
+
file_selector: FileSelector::JavaScript.excluding(@excluded),
|
59
|
+
serializer:,
|
60
|
+
since: @since || @relative_period,
|
61
|
+
)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def churn = Churn::GitCalculator
|
68
|
+
|
69
|
+
def serializer
|
70
|
+
case @serializer
|
71
|
+
when :none
|
72
|
+
Serializer::None
|
73
|
+
when :csv
|
74
|
+
Serializer::CSV
|
75
|
+
when :graph
|
76
|
+
Serializer::Graph.new
|
77
|
+
when :summary
|
78
|
+
Serializer::Summary
|
79
|
+
when :pass_through
|
80
|
+
Serializer::PassThrough
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module Normal
|
5
|
+
module Serializer
|
6
|
+
module CSV
|
7
|
+
def self.serialize(result)
|
8
|
+
values_by_file = result[:values_by_file]
|
9
|
+
values_by_file.map do |file, values|
|
10
|
+
"#{file},#{values[0]},#{values[1]}\n"
|
11
|
+
end.join
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module Normal
|
5
|
+
module Serializer
|
6
|
+
class Graph
|
7
|
+
def initialize(template: Graph.load_template_file)
|
8
|
+
@template = template
|
9
|
+
end
|
10
|
+
|
11
|
+
def serialize(result)
|
12
|
+
data = result[:values_by_file].map do |file, values|
|
13
|
+
"{ file_path: '#{file}', churn: #{values[0]}, complexity: #{values[1]} }"
|
14
|
+
end.join(",\n") + "\n"
|
15
|
+
title = Serializer.title(result)
|
16
|
+
@template.gsub("// INSERT DATA\n", data).gsub('INSERT TITLE', title)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.load_template_file
|
20
|
+
file_path = File.join(ROOT_PATH, 'tmp/template/graph.html')
|
21
|
+
File.read(file_path)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module Normal
|
5
|
+
module Serializer
|
6
|
+
module PassThrough
|
7
|
+
class << self
|
8
|
+
def serialize(result)
|
9
|
+
values_by_file = result[:values_by_file]
|
10
|
+
end_date = result[:git_period].end_date
|
11
|
+
values = values_by_file.map do |_, values|
|
12
|
+
[values[0].to_f, values[1].to_f]
|
13
|
+
end
|
14
|
+
{
|
15
|
+
end_date:,
|
16
|
+
values:,
|
17
|
+
}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module Normal
|
5
|
+
module Serializer
|
6
|
+
module Summary
|
7
|
+
def self.serialize(result)
|
8
|
+
values_by_file = result[:values_by_file]
|
9
|
+
summary = SummaryHash.serialize(result)
|
10
|
+
|
11
|
+
<<~SUMMARY
|
12
|
+
#{Serializer.title(result)}
|
13
|
+
|
14
|
+
Number of observations: #{values_by_file.size}
|
15
|
+
|
16
|
+
Churn:
|
17
|
+
Mean #{summary[:mean_churn]}, Median #{summary[:median_churn]}
|
18
|
+
|
19
|
+
Complexity:
|
20
|
+
Mean #{summary[:mean_complexity]}, Median #{summary[:median_complexity]}
|
21
|
+
|
22
|
+
Gamma score:
|
23
|
+
Mean #{summary[:mean_gamma_score]}, Median #{summary[:median_gamma_score]}
|
24
|
+
SUMMARY
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module Normal
|
5
|
+
module Serializer
|
6
|
+
module SummaryHash
|
7
|
+
class << self
|
8
|
+
def serialize(result)
|
9
|
+
values_by_file = result[:values_by_file]
|
10
|
+
churn_values = values_by_file.map { |_, values| values[0].to_f }
|
11
|
+
complexity_values = values_by_file.map { |_, values| values[1].to_f }
|
12
|
+
|
13
|
+
mean_churn = churn_values.sum / churn_values.size
|
14
|
+
median_churn = churn_values.sort[churn_values.size / 2]
|
15
|
+
mean_complexity = complexity_values.sum / complexity_values.size
|
16
|
+
median_complexity = complexity_values.sort[complexity_values.size / 2]
|
17
|
+
|
18
|
+
max_churn = churn_values.max
|
19
|
+
min_churn = churn_values.min
|
20
|
+
max_complexity = complexity_values.max
|
21
|
+
min_complexity = complexity_values.min
|
22
|
+
|
23
|
+
epsilon = 0.0001
|
24
|
+
gamma_score = values_by_file.map do |_, values|
|
25
|
+
# unnormalised harmonic mean of churn and complexity,
|
26
|
+
# since the summary needs to be comparable over time
|
27
|
+
churn = values[0].to_f + epsilon
|
28
|
+
complexity = values[1].to_f + epsilon
|
29
|
+
|
30
|
+
(2 * churn * complexity) / (churn + complexity)
|
31
|
+
end
|
32
|
+
|
33
|
+
mean_gamma_score = gamma_score.sum / gamma_score.size
|
34
|
+
median_gamma_score = gamma_score.sort[gamma_score.size / 2]
|
35
|
+
|
36
|
+
end_date = result[:git_period].end_date
|
37
|
+
|
38
|
+
{
|
39
|
+
mean_churn:,
|
40
|
+
median_churn:,
|
41
|
+
max_churn:,
|
42
|
+
min_churn:,
|
43
|
+
mean_complexity:,
|
44
|
+
median_complexity:,
|
45
|
+
max_complexity:,
|
46
|
+
min_complexity:,
|
47
|
+
mean_gamma_score:,
|
48
|
+
median_gamma_score:,
|
49
|
+
end_date:,
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'serializer/summary_hash'
|
4
|
+
require_relative 'serializer/summary'
|
5
|
+
require_relative 'serializer/csv'
|
6
|
+
require_relative 'serializer/graph'
|
7
|
+
require_relative 'serializer/pass_through'
|
8
|
+
|
9
|
+
module ChurnVsComplexity
|
10
|
+
module Normal
|
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
|
+
extend self
|
24
|
+
|
25
|
+
def serialize(result) = result
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|