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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +19 -2
  4. data/TODO +11 -0
  5. data/bin/churn_vs_complexity +5 -0
  6. data/lib/churn_vs_complexity/churn.rb +7 -2
  7. data/lib/churn_vs_complexity/cli.rb +21 -5
  8. data/lib/churn_vs_complexity/complexity/eslint_calculator.rb +34 -0
  9. data/lib/churn_vs_complexity/complexity/flog_calculator.rb +7 -6
  10. data/lib/churn_vs_complexity/complexity/pmd_calculator.rb +5 -2
  11. data/lib/churn_vs_complexity/complexity.rb +1 -0
  12. data/lib/churn_vs_complexity/concurrent_calculator.rb +2 -4
  13. data/lib/churn_vs_complexity/config.rb +81 -17
  14. data/lib/churn_vs_complexity/file_selector.rb +12 -1
  15. data/lib/churn_vs_complexity/git_date.rb +8 -1
  16. data/lib/churn_vs_complexity/serializer/csv.rb +14 -0
  17. data/lib/churn_vs_complexity/serializer/graph.rb +24 -0
  18. data/lib/churn_vs_complexity/serializer/pass_through.rb +21 -0
  19. data/lib/churn_vs_complexity/serializer/summary.rb +27 -0
  20. data/lib/churn_vs_complexity/serializer/summary_hash.rb +54 -0
  21. data/lib/churn_vs_complexity/serializer/timetravel/quality_calculator.rb +38 -0
  22. data/lib/churn_vs_complexity/serializer/timetravel/stats_calculator.rb +60 -0
  23. data/lib/churn_vs_complexity/serializer/timetravel.rb +103 -0
  24. data/lib/churn_vs_complexity/serializer.rb +7 -60
  25. data/lib/churn_vs_complexity/timetravel/traveller.rb +66 -0
  26. data/lib/churn_vs_complexity/timetravel/worktree.rb +56 -0
  27. data/lib/churn_vs_complexity/timetravel.rb +70 -0
  28. data/lib/churn_vs_complexity/version.rb +1 -1
  29. data/lib/churn_vs_complexity.rb +2 -0
  30. data/package-lock.json +6 -0
  31. data/tmp/eslint-support/complexity-calculator.js +51 -0
  32. data/tmp/eslint-support/package.json +11 -0
  33. data/tmp/template/graph.html +1 -4
  34. data/tmp/template/timetravel_graph.html +100 -0
  35. data/tmp/test-support/javascript/complex.js +43 -0
  36. data/tmp/test-support/javascript/moderate.js +12 -0
  37. data/tmp/test-support/javascript/simple.js +5 -0
  38. data/tmp/test-support/javascript/typescript-example.ts +26 -0
  39. data/tmp/timetravel/.keep +0 -0
  40. metadata +24 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d748bfb7d21087a5fbcb360dbc05dcd46b5ec05bf2c2fcc542e2e25368084d78
4
- data.tar.gz: 0d160c582aee2696823061dd7623cf17e9b537d798ae31502eb30b065a61391d
3
+ metadata.gz: 7484ff3a1c015738808226a78087017f6b7aff5ce42d15879023f32df5648717
4
+ data.tar.gz: ad3bdeff5ba32e9d7f414b45173d8928b1c463313557202b96aaf5fdcf059109
5
5
  SHA512:
6
- metadata.gz: f521d39fddad8e8adf05ab0f21d8b8adcc2460f9a57c553b90c73eb60451aafed19ab11977768d494987ff72838c856dd04c3910781ec0da34f53d82d18d9d4e
7
- data.tar.gz: 3ab17db013540128bd428edb54cf0615f47ce50caa19d1ad418261c20efde494bb36ea8e84408e0ca330f7721013a9e8958f3cd5a16565636186508e59bf7360
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 was built primarily to support analysis of Java and Ruby repositories, but it can easily be extended.
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
@@ -6,6 +6,11 @@ require 'churn_vs_complexity'
6
6
 
7
7
  begin
8
8
  ChurnVsComplexity::CLI.run!
9
+ rescue ChurnVsComplexity::ValidationError => e
10
+ warn e.message
11
+ exit 1
9
12
  rescue StandardError => e
13
+ warn e.backtrace
10
14
  warn e.message
15
+ exit 2
11
16
  end
@@ -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
- formatted_date = since.strftime('%Y-%m-%d')
12
- cmd = %Q(git --git-dir #{git_dir} --work-tree #{folder} log --format="%H" --follow --since="#{formatted_date}" -- #{file} | wc -l)
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', 'Calculate churn after this date') do |value|
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[:since] = :month
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[:since] = :quarter
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[:since] = :year
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
- puts config.to_engine.check(folder:)
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(file:)
14
- flog = Flog.new
15
- flog.flog(file)
16
- { file => flog.total_score }
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
- output = `pmd check -d #{folder} -R #{resolve_ruleset_path} -f json -t #{CONCURRENCY} --cache #{resolve_cache_path}`
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', 'pmd-cache')
35
+ File.join(gem_root, 'tmp', 'pmd-support', "pmd-cache-#{Process.pid}")
33
36
  end
34
37
 
35
38
  def gem_root
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'complexity/pmd_calculator'
4
4
  require_relative 'complexity/flog_calculator'
5
+ require_relative 'complexity/eslint_calculator'
5
6
 
6
7
  module ChurnVsComplexity
7
8
  module Complexity
@@ -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].each_with_object({}) do |file, acc|
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 Error, "Unsupported language: #{@language}" unless %i[java ruby].include?(@language)
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
- @since_validator.validate!(@since)
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
- if since.is_a?(Symbol)
82
- raise Error, "Invalid since value #{since}" unless %i[month quarter year].include?(since)
83
- elsif since.is_a?(String)
84
- begin
85
- Date.strptime(since, '%Y-%m-%d')
86
- rescue StandardError
87
- raise Error, "Invalid date #{since}, please use correct format, YYYY-MM-DD"
88
- end
89
- else
90
- raise Error, "Invalid since value #{since}"
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