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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +29 -14
  4. data/README.md +10 -4
  5. data/lib/churn_vs_complexity/churn.rb +9 -1
  6. data/lib/churn_vs_complexity/cli/main.rb +46 -0
  7. data/lib/churn_vs_complexity/cli/parser.rb +91 -0
  8. data/lib/churn_vs_complexity/cli.rb +11 -94
  9. data/lib/churn_vs_complexity/complexity/eslint_calculator.rb +6 -0
  10. data/lib/churn_vs_complexity/complexity/pmd/files_calculator.rb +26 -0
  11. data/lib/churn_vs_complexity/complexity/pmd/folder_calculator.rb +20 -0
  12. data/lib/churn_vs_complexity/complexity/{pmd_calculator.rb → pmd.rb} +14 -21
  13. data/lib/churn_vs_complexity/complexity.rb +1 -1
  14. data/lib/churn_vs_complexity/complexity_validator.rb +14 -0
  15. data/lib/churn_vs_complexity/concurrent_calculator.rb +5 -3
  16. data/lib/churn_vs_complexity/delta/checker.rb +54 -0
  17. data/lib/churn_vs_complexity/delta/commit_hydrator.rb +22 -0
  18. data/lib/churn_vs_complexity/delta/complexity_annotator.rb +30 -0
  19. data/lib/churn_vs_complexity/delta/config.rb +50 -0
  20. data/lib/churn_vs_complexity/delta/factory.rb +22 -0
  21. data/lib/churn_vs_complexity/delta/multi_checker.rb +48 -0
  22. data/lib/churn_vs_complexity/delta/serializer.rb +69 -0
  23. data/lib/churn_vs_complexity/delta.rb +52 -0
  24. data/lib/churn_vs_complexity/engine.rb +1 -1
  25. data/lib/churn_vs_complexity/file_selector.rb +47 -4
  26. data/lib/churn_vs_complexity/git_strategy.rb +62 -0
  27. data/lib/churn_vs_complexity/language_validator.rb +9 -0
  28. data/lib/churn_vs_complexity/normal/config.rb +85 -0
  29. data/lib/churn_vs_complexity/normal/serializer/csv.rb +16 -0
  30. data/lib/churn_vs_complexity/normal/serializer/graph.rb +26 -0
  31. data/lib/churn_vs_complexity/normal/serializer/pass_through.rb +23 -0
  32. data/lib/churn_vs_complexity/normal/serializer/summary.rb +29 -0
  33. data/lib/churn_vs_complexity/normal/serializer/summary_hash.rb +56 -0
  34. data/lib/churn_vs_complexity/normal/serializer.rb +29 -0
  35. data/lib/churn_vs_complexity/normal.rb +45 -0
  36. data/lib/churn_vs_complexity/timetravel/config.rb +75 -0
  37. data/lib/churn_vs_complexity/timetravel/factory.rb +12 -0
  38. data/lib/churn_vs_complexity/{serializer/timetravel → timetravel/serializer}/quality_calculator.rb +2 -2
  39. data/lib/churn_vs_complexity/{serializer/timetravel → timetravel/serializer}/stats_calculator.rb +2 -2
  40. data/lib/churn_vs_complexity/{serializer/timetravel.rb → timetravel/serializer.rb} +6 -6
  41. data/lib/churn_vs_complexity/timetravel/traveller.rb +5 -11
  42. data/lib/churn_vs_complexity/timetravel/worktree.rb +30 -14
  43. data/lib/churn_vs_complexity/timetravel.rb +36 -39
  44. data/lib/churn_vs_complexity/version.rb +1 -1
  45. data/lib/churn_vs_complexity.rb +23 -7
  46. data/tmp/test-support/delta/ruby-summary.txt +50 -0
  47. data/tmp/test-support/delta/ruby.csv +12 -0
  48. metadata +38 -20
  49. data/.travis.yml +0 -7
  50. data/lib/churn_vs_complexity/config.rb +0 -159
  51. data/lib/churn_vs_complexity/serializer/csv.rb +0 -14
  52. data/lib/churn_vs_complexity/serializer/graph.rb +0 -24
  53. data/lib/churn_vs_complexity/serializer/pass_through.rb +0 -21
  54. data/lib/churn_vs_complexity/serializer/summary.rb +0 -27
  55. data/lib/churn_vs_complexity/serializer/summary_hash.rb +0 -54
  56. 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
- Dir.glob("#{folder}/**/*").each do |f|
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(['.java'], excluded)
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(['.rb'], excluded)
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(['.js', '.jsx', '.ts', '.tsx'], excluded, true)
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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChurnVsComplexity
4
+ module LanguageValidator
5
+ def self.validate!(language)
6
+ raise ValidationError, "Unsupported language: #{language}" unless %i[java ruby javascript].include?(language)
7
+ end
8
+ end
9
+ 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