churn_vs_complexity 1.4.0 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +24 -14
  4. data/README.md +7 -4
  5. data/lib/churn_vs_complexity/churn.rb +9 -1
  6. data/lib/churn_vs_complexity/cli/parser.rb +91 -0
  7. data/lib/churn_vs_complexity/cli.rb +32 -90
  8. data/lib/churn_vs_complexity/complexity/eslint_calculator.rb +6 -0
  9. data/lib/churn_vs_complexity/complexity/pmd/files_calculator.rb +26 -0
  10. data/lib/churn_vs_complexity/complexity/pmd/folder_calculator.rb +20 -0
  11. data/lib/churn_vs_complexity/complexity/{pmd_calculator.rb → pmd.rb} +14 -21
  12. data/lib/churn_vs_complexity/complexity.rb +1 -1
  13. data/lib/churn_vs_complexity/complexity_validator.rb +14 -0
  14. data/lib/churn_vs_complexity/concurrent_calculator.rb +5 -3
  15. data/lib/churn_vs_complexity/delta/checker.rb +56 -0
  16. data/lib/churn_vs_complexity/delta/complexity_annotator.rb +30 -0
  17. data/lib/churn_vs_complexity/delta/config.rb +50 -0
  18. data/lib/churn_vs_complexity/delta/factory.rb +22 -0
  19. data/lib/churn_vs_complexity/delta/multi_checker.rb +48 -0
  20. data/lib/churn_vs_complexity/delta/serializer.rb +69 -0
  21. data/lib/churn_vs_complexity/delta.rb +51 -0
  22. data/lib/churn_vs_complexity/engine.rb +1 -1
  23. data/lib/churn_vs_complexity/file_selector.rb +47 -4
  24. data/lib/churn_vs_complexity/git_strategy.rb +60 -0
  25. data/lib/churn_vs_complexity/language_validator.rb +9 -0
  26. data/lib/churn_vs_complexity/normal/config.rb +85 -0
  27. data/lib/churn_vs_complexity/normal/serializer/csv.rb +16 -0
  28. data/lib/churn_vs_complexity/normal/serializer/graph.rb +26 -0
  29. data/lib/churn_vs_complexity/normal/serializer/pass_through.rb +23 -0
  30. data/lib/churn_vs_complexity/normal/serializer/summary.rb +29 -0
  31. data/lib/churn_vs_complexity/normal/serializer/summary_hash.rb +56 -0
  32. data/lib/churn_vs_complexity/normal/serializer.rb +29 -0
  33. data/lib/churn_vs_complexity/normal.rb +45 -0
  34. data/lib/churn_vs_complexity/timetravel/config.rb +75 -0
  35. data/lib/churn_vs_complexity/timetravel/factory.rb +12 -0
  36. data/lib/churn_vs_complexity/{serializer/timetravel → timetravel/serializer}/quality_calculator.rb +2 -2
  37. data/lib/churn_vs_complexity/{serializer/timetravel → timetravel/serializer}/stats_calculator.rb +2 -2
  38. data/lib/churn_vs_complexity/{serializer/timetravel.rb → timetravel/serializer.rb} +6 -6
  39. data/lib/churn_vs_complexity/timetravel/traveller.rb +5 -11
  40. data/lib/churn_vs_complexity/timetravel/worktree.rb +30 -14
  41. data/lib/churn_vs_complexity/timetravel.rb +36 -39
  42. data/lib/churn_vs_complexity/version.rb +1 -1
  43. data/lib/churn_vs_complexity.rb +23 -7
  44. data/tmp/test-support/delta/ruby-summary.txt +50 -0
  45. data/tmp/test-support/delta/ruby.csv +12 -0
  46. metadata +36 -20
  47. data/.travis.yml +0 -7
  48. data/lib/churn_vs_complexity/config.rb +0 -159
  49. data/lib/churn_vs_complexity/serializer/csv.rb +0 -14
  50. data/lib/churn_vs_complexity/serializer/graph.rb +0 -24
  51. data/lib/churn_vs_complexity/serializer/pass_through.rb +0 -21
  52. data/lib/churn_vs_complexity/serializer/summary.rb +0 -27
  53. data/lib/churn_vs_complexity/serializer/summary_hash.rb +0 -54
  54. data/lib/churn_vs_complexity/serializer.rb +0 -26
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7484ff3a1c015738808226a78087017f6b7aff5ce42d15879023f32df5648717
4
- data.tar.gz: ad3bdeff5ba32e9d7f414b45173d8928b1c463313557202b96aaf5fdcf059109
3
+ metadata.gz: dba0d67852cef763abe50d7fa5a63a7e9b4687b5232d05b281ed267cf948a0c4
4
+ data.tar.gz: 1839953429d9840ce96782431d530f45e8169441e54c94badf9f12fc9aea7b0a
5
5
  SHA512:
6
- metadata.gz: a56e26296acfff22e755c414cab9ee36923e4ccd7181d67691f55feb8f99b5aa738f5d2b7645006f25c632efa346fb535d7fe04923443dedf607772ce1a21323
7
- data.tar.gz: f406ee696facf7708e792b67ed8b463f1554365b3939deb4bde40c9fab88dfe891997f611727559a283103c5b7234f71d78d5fe108731628cb3fd8a782a73ce5
6
+ metadata.gz: 699fad42098c2a7e9e4e1368a03fda265ef3333b0e9c0895f6095e38898cf68a36a1c15d152de98f39a4f2a3ce866ce481bea68f83033876aa0d400cfe024b57
7
+ data.tar.gz: dd4a1412a7c49e76c7dd7624e80f5bd06f3c5bb027402f3eba8f9842568db3f4f6a2e1f6b9b5a559af6eee61c3049e7cd8848640432e3929f4b554b9c28dc847
data/.rubocop.yml CHANGED
@@ -9,6 +9,9 @@ Layout/LineLength:
9
9
  Style/Documentation:
10
10
  Enabled: false
11
11
 
12
+ Style/ModuleFunction:
13
+ Enabled: false
14
+
12
15
  Style/TrailingCommaInArrayLiteral:
13
16
  Enabled: true
14
17
  EnforcedStyleForMultiline: consistent_comma
data/CHANGELOG.md CHANGED
@@ -1,25 +1,35 @@
1
- ## [1.0.0] - 2024-06-07
1
+ ## [1.5.1] - 2024-10-15
2
2
 
3
- - Initial release
3
+ - Fix bug where worktree checkout silently failed
4
4
 
5
- ## [1.1.0] - 2024-09-20
6
5
 
7
- - Introduce `--summary` flag to output summary statistics for churn and complexity
8
- - Introduce `--month`, `--quarter`, and `--year` short-hand flags to calculate churn for different time periods relative to the most recent commit
6
+ ## [1.5.0] - 2024-10-14
9
7
 
10
- ## [1.2.0] - 2024-09-20
8
+ - Added delta mode to annotate changes in individual commits with complexity
9
+ - Moving PMD cache to the OS temp directory
11
10
 
12
- - Fix bug in CLI where new flags and `--since` would not be recognized
13
- - Improve selection of observations included in the output
14
- - Fixed calculation of churn that would never be zero
11
+ ## [1.4.0] - 2024-10-10
12
+
13
+ - Added timetravel mode to visualise code quality over time
14
+ - Added alpha, beta, and gamma scores to summaries
15
+ - Fixed broken Ruby complexity calculation
15
16
 
16
17
  ## [1.3.0] - 2024-09-26
17
18
 
18
- - Add support for javascript and typescript complexity calculation using eslint
19
+ - Added support for javascript and typescript complexity calculation using eslint
20
+
21
+ ## [1.2.0] - 2024-09-20
22
+
23
+ - Fixed bug in CLI where new flags and `--since` would not be recognized
24
+ - Improved selection of observations included in the output
25
+ - Fixed calculation of churn that would never be zero
19
26
  - Fixed behavior when --since or short-hand flags were not provided
20
27
 
21
- ## [1.4.0] - 2024-10-10
28
+ ## [1.1.0] - 2024-09-20
29
+
30
+ - Introduced `--summary` flag to output summary statistics for churn and complexity
31
+ - Introduced `--month`, `--quarter`, and `--year` short-hand flags to calculate churn for different time periods relative to the most recent commit
32
+
33
+ ## [1.0.0] - 2024-06-07
22
34
 
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
35
+ - Initial release
data/README.md CHANGED
@@ -22,9 +22,11 @@ Or install it yourself as:
22
22
 
23
23
  $ gem install churn_vs_complexity
24
24
 
25
- This gem depends on git for churn analysis and [PMD](https://pmd.github.io) for complexity analysis of JVM based languages.
25
+ This gem depends on git for churn analysis.
26
26
 
27
- In order to use the `--java` flag, you must first install PMD manually, and the gem assumes it is available on the search path as `pmd`. On macOS, for example, you can install it using homebrew with `brew install pmd`.
27
+ Complexity analysis for Java relies on [PMD](https://pmd.github.io). In order to use the `--java` flag, you must first install PMD manually, and the gem assumes it is available on the search path as `pmd`. On macOS, for example, you can install it using homebrew with `brew install pmd`.
28
+
29
+ Complexity analysis for JavaScript and TypeScript relies on [ESLint](https://eslint.org). In order to use the `--js`, `--ts`, `--javascript`, or `--typescript` flag, you must have Node.js installed.
28
30
 
29
31
  ## Usage
30
32
 
@@ -45,10 +47,9 @@ Usage: churn_vs_complexity [options] folder
45
47
  -q, --quarter Calculate churn for the quarter leading up to the most recent commit
46
48
  -y, --year Calculate churn for the year leading up to the most recent commit
47
49
  --timetravel N Calculate summary for all commits at intervals of N days throughout project history or from the date specified with --since
50
+ --delta SHA Identify changes between the specified commit and the previous commit and annotate changed files with complexity score. Can be used multiple times to specify multiple commits.
48
51
  --dry-run Echo the chosen options from the CLI
49
52
  -h, --help Display help
50
-
51
-
52
53
  ```
53
54
 
54
55
  Note that when using the `--timetravel` mode, the semantics of some flags are subtly different from normal mode:
@@ -71,6 +72,8 @@ Summary points in timetravel mode instead include an alpha score, which is the s
71
72
 
72
73
  `churn_vs_complexity --java -m --since 2019-03-01 --timetravel 30 --graph my_java_project > ~/Desktop/timetravel-after-1st-march-2019.html`
73
74
 
75
+ `churn_vs_complexity --delta 1496402e81e68e86c5ac240559099fbe581a9a2g --delta 2845296758861773778d70d96328a5f2a1a9e933 --js --summary my_javascript_project > ~/Desktop/interesting-commits.txt`
76
+
74
77
  ## Development
75
78
 
76
79
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -4,6 +4,14 @@ require 'git'
4
4
 
5
5
  module ChurnVsComplexity
6
6
  module Churn
7
+ module Disabled
8
+ def self.calculate(*)
9
+ raise Error, 'Churn is disabled'
10
+ end
11
+
12
+ def self.date_of_latest_commit(*) = :disabled
13
+ end
14
+
7
15
  module GitCalculator
8
16
  class << self
9
17
  def calculate(folder:, file:, since:)
@@ -11,7 +19,7 @@ module ChurnVsComplexity
11
19
  earliest_date = [date_of_first_commit(folder:), since].max
12
20
  formatted_date = earliest_date.strftime('%Y-%m-%d')
13
21
  cmd = %(git --git-dir #{git_dir} --work-tree #{folder} log --format="%H" --follow --since="#{formatted_date}" -- #{file} | wc -l)
14
- `#{cmd}`.to_i
22
+ `(#{cmd}) 2>/dev/null`.to_i
15
23
  end
16
24
 
17
25
  def date_of_latest_commit(folder:)
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+
5
+ module ChurnVsComplexity
6
+ module CLI
7
+ module Parser
8
+ def self.create
9
+ options = { excluded: [] }
10
+ parser = OptionParser.new do |opts|
11
+ opts.banner = 'Usage: churn_vs_complexity [options] folder'
12
+
13
+ opts.on('--java', 'Check complexity of java classes') do
14
+ options[:language] = :java
15
+ end
16
+
17
+ opts.on('--ruby', 'Check complexity of ruby files') do
18
+ options[:language] = :ruby
19
+ end
20
+
21
+ opts.on('--js', '--ts', '--javascript', '--typescript',
22
+ 'Check complexity of javascript and typescript files',) do
23
+ options[:language] = :javascript
24
+ end
25
+
26
+ opts.on('--csv', 'Format output as CSV') do
27
+ options[:serializer] = :csv
28
+ end
29
+
30
+ opts.on('--graph', 'Format output as HTML page with Churn vs Complexity graph') do
31
+ options[:serializer] = :graph
32
+ end
33
+
34
+ opts.on('--summary', 'Output summary statistics (mean and median) for churn and complexity') do
35
+ options[:serializer] = :summary
36
+ end
37
+
38
+ opts.on('--excluded PATTERN',
39
+ 'Exclude file paths including this string. Can be used multiple times.',) do |value|
40
+ options[:excluded] << value
41
+ end
42
+
43
+ opts.on('--since YYYY-MM-DD',
44
+ 'Normal mode: Calculate churn after this date. Timetravel mode: calculate summaries from this date',) do |value|
45
+ options[:since] = value
46
+ end
47
+
48
+ opts.on('-m', '--month', 'Calculate churn for the month leading up to the most recent commit') do
49
+ options[:relative_period] = :month
50
+ end
51
+
52
+ opts.on('-q', '--quarter', 'Calculate churn for the quarter leading up to the most recent commit') do
53
+ options[:relative_period] = :quarter
54
+ end
55
+
56
+ opts.on('-y', '--year', 'Calculate churn for the year leading up to the most recent commit') do
57
+ options[:relative_period] = :year
58
+ end
59
+
60
+ opts.on('--timetravel N',
61
+ 'Calculate summary for all commits at intervals of N days throughout project history or from the date specified with --since',) do |value|
62
+ options[:mode] = :timetravel
63
+ options[:jump_days] = value.to_i
64
+ end
65
+
66
+ opts.on('--delta SHA',
67
+ 'Identify changes between the specified commit and the previous commit and annotate changed files with complexity score. Can be used multiple times to specify multiple commits.',) do |value|
68
+ options[:mode] = :delta
69
+ (options[:commits] ||= []) << value
70
+ end
71
+
72
+ opts.on('--dry-run', 'Echo the chosen options from the CLI') do
73
+ puts options
74
+ exit
75
+ end
76
+
77
+ opts.on('-h', '--help', 'Display help') do
78
+ puts opts
79
+ exit
80
+ end
81
+
82
+ opts.on('--version', 'Display version') do
83
+ puts ChurnVsComplexity::VERSION
84
+ exit
85
+ end
86
+ end
87
+ [parser, options]
88
+ end
89
+ end
90
+ end
91
+ end
@@ -1,101 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'date'
4
- require 'time'
5
- require 'optparse'
3
+ require_relative 'cli/parser'
6
4
 
7
5
  module ChurnVsComplexity
8
- class CLI
9
- def self.run!
10
- # Create an options hash to store parsed options
11
- options = { excluded: [] }
12
-
13
- # Initialize OptionParser
14
- OptionParser.new do |opts|
15
- opts.banner = 'Usage: churn_vs_complexity [options] folder'
16
-
17
- opts.on('--java', 'Check complexity of java classes') do
18
- options[:language] = :java
19
- end
20
-
21
- opts.on('--ruby', 'Check complexity of ruby files') do
22
- options[:language] = :ruby
23
- end
24
-
25
- opts.on('--js', '--ts', '--javascript', '--typescript',
26
- 'Check complexity of javascript and typescript files',) do
27
- options[:language] = :javascript
28
- end
29
-
30
- opts.on('--csv', 'Format output as CSV') do
31
- options[:serializer] = :csv
32
- end
33
-
34
- opts.on('--graph', 'Format output as HTML page with Churn vs Complexity graph') do
35
- options[:serializer] = :graph
36
- end
37
-
38
- opts.on('--summary', 'Output summary statistics (mean and median) for churn and complexity') do
39
- options[:serializer] = :summary
40
- end
41
-
42
- opts.on('--excluded PATTERN',
43
- 'Exclude file paths including this string. Can be used multiple times.',) do |value|
44
- options[:excluded] << value
45
- end
46
-
47
- opts.on('--since YYYY-MM-DD',
48
- 'Normal mode: Calculate churn after this date. Timetravel mode: calculate summaries from this date',) do |value|
49
- options[:since] = value
50
- end
51
-
52
- opts.on('-m', '--month', 'Calculate churn for the month leading up to the most recent commit') do
53
- options[:relative_period] = :month
54
- end
55
-
56
- opts.on('-q', '--quarter', 'Calculate churn for the quarter leading up to the most recent commit') do
57
- options[:relative_period] = :quarter
58
- end
59
-
60
- opts.on('-y', '--year', 'Calculate churn for the year leading up to the most recent commit') do
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
68
- end
69
-
70
- opts.on('--dry-run', 'Echo the chosen options from the CLI') do
71
- puts options
72
- exit
73
- end
74
-
75
- opts.on('-h', '--help', 'Display help') do
76
- puts opts
77
- exit
78
- end
79
- end.parse!
80
-
81
- # First argument that is not an option is the folder
82
- folder = ARGV.first
83
-
84
- raise Error, 'No folder selected. Use --help for usage information.' if folder.nil? || folder.empty?
85
-
86
- #  Verify that folder exists
87
- raise Error, "Folder #{folder} does not exist" unless File.directory?(folder)
6
+ module CLI
7
+ class << self
8
+ def run!
9
+ parser, options = Parser.create
10
+ parser.parse!
11
+ # First argument that is not an option is the folder
12
+ folder = ARGV.first
13
+
14
+ validate_folder!(folder)
15
+ validate_options!(options)
16
+ config = config(options)
17
+ config.validate!
18
+ puts config.checker.check(folder:)
19
+ end
88
20
 
89
- raise Error, 'No options selected. Use --help for usage information.' if options.empty?
21
+ private
90
22
 
91
- config = Config.new(**options)
23
+ def validate_folder!(folder)
24
+ raise Error, 'No folder selected. Use --help for usage information.' if folder.nil? || folder.empty?
25
+ raise Error, "Folder #{folder} does not exist" unless File.directory?(folder)
26
+ end
92
27
 
93
- config.validate!
28
+ def validate_options!(options)
29
+ raise Error, 'No options selected. Use --help for usage information.' if options.empty?
30
+ raise Error, 'No language selected. Use --help for usage information.' if options[:language].nil?
31
+ raise Error, 'No serializer selected. Use --help for usage information.' if options[:serializer].nil?
32
+ end
94
33
 
95
- if options[:mode] == :timetravel
96
- puts config.timetravel.go(folder:)
97
- else
98
- puts config.to_engine.check(folder:)
34
+ def config(options)
35
+ config_class = case options[:mode]
36
+ when :timetravel then Timetravel::Config
37
+ when :delta then Delta::Config
38
+ else Normal::Config
39
+ end
40
+ config_class.new(**options)
99
41
  end
100
42
  end
101
43
  end
@@ -23,6 +23,12 @@ module ChurnVsComplexity
23
23
  end
24
24
  end
25
25
 
26
+ def check_dependencies!
27
+ `npm --version`
28
+ rescue Errno::ENOENT
29
+ raise Error, 'Needs node and npm installed'
30
+ end
31
+
26
32
  private
27
33
 
28
34
  def gem_root
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChurnVsComplexity
4
+ module Complexity
5
+ module PMD
6
+ class FilesCalculator
7
+ def initialize(cache_components:)
8
+ @cache_components = cache_components
9
+ end
10
+
11
+ def folder_based? = false
12
+
13
+ def calculate(files:)
14
+ return Parser.empty_result if files.empty?
15
+
16
+ cache_path = PMD.resolve_cache_path(*@cache_components)
17
+ files_arg = files.map { |file| "-d #{file}" }.join(' ')
18
+ command = "pmd check #{files_arg} -R #{PMD.resolve_ruleset_path} -f json -t #{CONCURRENCY} --cache #{cache_path} 2>/dev/null"
19
+ output = `#{command}`
20
+
21
+ Parser.new.parse(output)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChurnVsComplexity
4
+ module Complexity
5
+ module PMD
6
+ module FolderCalculator
7
+ class << self
8
+ def folder_based? = true
9
+
10
+ def calculate(folder:)
11
+ cache_path = PMD.resolve_cache_path(folder)
12
+ output = `pmd check -d #{folder} -R #{PMD.resolve_ruleset_path} -f json -t #{CONCURRENCY} --cache #{cache_path} 2>/dev/null`
13
+
14
+ Parser.new.parse(output)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,19 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'pmd/folder_calculator'
4
+ require_relative 'pmd/files_calculator'
5
+
3
6
  module ChurnVsComplexity
4
7
  module Complexity
5
- module PMDCalculator
8
+ module PMD
6
9
  CONCURRENCY = Etc.nprocessors
7
10
 
8
11
  class << self
9
- def folder_based? = true
12
+ def resolve_ruleset_path
13
+ ruleset_path = File.join(gem_root, 'tmp', 'pmd-support', 'ruleset.xml')
14
+ raise "ruleset.xml not found in #{ruleset_path}" unless File.exist?(ruleset_path)
10
15
 
11
- def calculate(folder:)
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)
16
+ ruleset_path
17
+ end
15
18
 
16
- Parser.new.parse(output)
19
+ def resolve_cache_path(*components)
20
+ File.join(ChurnVsComplexity.tmp_dir_path(*components), 'pmd', 'pmd-cache')
17
21
  end
18
22
 
19
23
  def check_dependencies!
@@ -24,23 +28,12 @@ module ChurnVsComplexity
24
28
 
25
29
  private
26
30
 
27
- def resolve_ruleset_path
28
- ruleset_path = File.join(gem_root, 'tmp', 'pmd-support', 'ruleset.xml')
29
- raise "ruleset.xml not found in #{ruleset_path}" unless File.exist?(ruleset_path)
30
-
31
- ruleset_path
32
- end
33
-
34
- def resolve_cache_path
35
- File.join(gem_root, 'tmp', 'pmd-support', "pmd-cache-#{Process.pid}")
36
- end
37
-
38
- def gem_root
39
- File.expand_path('../../..', __dir__)
40
- end
31
+ def gem_root = ROOT_PATH
41
32
  end
42
33
 
43
34
  class Parser
35
+ def self.empty_result = {}
36
+
44
37
  def parse(output)
45
38
  doc = JSON.parse(output)
46
39
  doc['files'].each_with_object({}) do |file, result|
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'complexity/pmd_calculator'
3
+ require_relative 'complexity/pmd'
4
4
  require_relative 'complexity/flog_calculator'
5
5
  require_relative 'complexity/eslint_calculator'
6
6
 
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChurnVsComplexity
4
+ module ComplexityValidator
5
+ def self.validate!(language)
6
+ case language
7
+ when :java
8
+ Complexity::PMD.check_dependencies!
9
+ when :javascript
10
+ Complexity::ESLintCalculator.check_dependencies!
11
+ end
12
+ end
13
+ end
14
+ end
@@ -14,8 +14,10 @@ module ChurnVsComplexity
14
14
 
15
15
  def calculate(folder:, files:, since:)
16
16
  latest_commit_date = @churn.date_of_latest_commit(folder:)
17
- @git_period = GitDate.git_period(since, latest_commit_date)
18
- schedule_churn_calculation(folder, files[:included], @git_period.effective_start_date)
17
+ unless latest_commit_date == :disabled
18
+ @git_period = GitDate.git_period(since, latest_commit_date)
19
+ schedule_churn_calculation(folder, files[:included], @git_period.effective_start_date)
20
+ end
19
21
  calculate_complexity(folder, files)
20
22
  await_results
21
23
  combine_results
@@ -49,7 +51,7 @@ module ChurnVsComplexity
49
51
  end
50
52
 
51
53
  def await_results
52
- @threads.each(&:join)
54
+ @threads&.each(&:join)
53
55
  rescue StandardError => e
54
56
  raise Error, "Failed to caculate churn: #{e.message}"
55
57
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChurnVsComplexity
4
+ module Delta
5
+ class Checker
6
+ def initialize(serializer:, factory:, commit:, language:, excluded:, data_isolation_id: 0)
7
+ @serializer = serializer
8
+ @excluded = excluded
9
+ @factory = factory
10
+ @commit = commit
11
+ @language = language
12
+ @data_isolation_id = data_isolation_id
13
+ end
14
+
15
+ def check(folder:)
16
+ raise Error, 'Invalid commit' unless valid_commit?(folder:)
17
+
18
+ worktree = setup_worktree(folder:)
19
+
20
+ changes = @factory.git_strategy(folder: worktree.folder).changes(commit: @commit)
21
+ result = commit_summary(folder:)
22
+ unless changes.empty?
23
+ ComplexityAnnotator.new(factory: @factory, changes:)
24
+ .enhance(worktree_folder: worktree.folder, language: @language, excluded: @excluded, commit: @commit)
25
+ result[:changes] = changes
26
+ end
27
+
28
+ @serializer.serialize(result)
29
+ end
30
+
31
+ private
32
+
33
+ def setup_worktree(folder:)
34
+ worktree = @factory.worktree(root_folder: folder, git_strategy: @factory.git_strategy(folder:),
35
+ data_isolation_id: @data_isolation_id,)
36
+ worktree.prepare
37
+ worktree.checkout(@commit)
38
+
39
+ worktree
40
+ end
41
+
42
+ def commit_summary(folder:)
43
+ summary = { commit: @commit }
44
+ if @serializer.respond_to?(:has_commit_summary?) && @serializer.has_commit_summary?
45
+ parent, next_commit = @factory.git_strategy(folder:).surrounding(commit: @commit)
46
+ summary.merge!(parent:, next_commit:)
47
+ end
48
+ summary
49
+ end
50
+
51
+ def valid_commit?(folder:)
52
+ @factory.git_strategy(folder:).valid_commit?(commit: @commit)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChurnVsComplexity
4
+ module Delta
5
+ class ComplexityAnnotator
6
+ def initialize(factory:, changes:)
7
+ @factory = factory
8
+ @changes = changes
9
+ end
10
+
11
+ def enhance(worktree_folder:, language:, excluded:, commit:)
12
+ @changes.each do |change|
13
+ change[:full_path] = File.join(worktree_folder, change[:path])
14
+ end
15
+
16
+ files = @changes.reject { |change| change[:type] == :deleted }.map { |change| change[:full_path] }
17
+
18
+ engine = @factory.engine(cache_components: [worktree_folder, commit], language:, excluded:, files:)
19
+
20
+ values_by_file = engine.check(folder: worktree_folder)[:values_by_file]
21
+
22
+ valid_extensions = FileSelector.extensions(language)
23
+ @changes.select! { |change| valid_extensions.any? { |ext| change[:path].end_with?(ext) } }
24
+ @changes.each do |annotated_file|
25
+ annotated_file[:complexity] = values_by_file.dig(annotated_file[:full_path], 1)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -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.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