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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 843cae8dad7f09344cbea477eb73ae3d9d57945df8a5955d93d6dfc820b5a8e1
|
4
|
+
data.tar.gz: 5066b98af73acde0f18b233fd12efe6858bb9977c4a9d553970c6ad859db98a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7af6d2c460fd9bb2cb78751d8a7f5cdd3ada48e9026e61ccbd3ef74a22c738bcfea4185984ffea2476a5d4b726f902c881edabf2baefa211884bfb129b59f80c
|
7
|
+
data.tar.gz: c3cf5e4dfd7bea34298fac9d7e2db1b7ffe58ad16acab25e9217dae0f84b43e2da7d89f147027f1ebb3fd52115538aba81a1f05bca5a0f7e13f0ad5d3a7b9aef
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,25 +1,40 @@
|
|
1
|
-
## [1.
|
1
|
+
## [1.5.2] - 2024-10-21
|
2
2
|
|
3
|
-
-
|
3
|
+
- Fixed bug where delta mode validations would fail when the commit was a non-sha value.
|
4
|
+
- Allow HEAD as specified commit in delta mode
|
4
5
|
|
5
|
-
## [1.1
|
6
|
+
## [1.5.1] - 2024-10-15
|
6
7
|
|
7
|
-
-
|
8
|
-
- Introduce `--month`, `--quarter`, and `--year` short-hand flags to calculate churn for different time periods relative to the most recent commit
|
8
|
+
- Fix bug where worktree checkout silently failed
|
9
9
|
|
10
|
-
## [1.2.0] - 2024-09-20
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
-
|
11
|
+
## [1.5.0] - 2024-10-14
|
12
|
+
|
13
|
+
- Added delta mode to annotate changes in individual commits with complexity
|
14
|
+
- Moving PMD cache to the OS temp directory
|
15
|
+
|
16
|
+
## [1.4.0] - 2024-10-10
|
17
|
+
|
18
|
+
- Added timetravel mode to visualise code quality over time
|
19
|
+
- Added alpha, beta, and gamma scores to summaries
|
20
|
+
- Fixed broken Ruby complexity calculation
|
15
21
|
|
16
22
|
## [1.3.0] - 2024-09-26
|
17
23
|
|
18
|
-
-
|
24
|
+
- Added support for javascript and typescript complexity calculation using eslint
|
25
|
+
|
26
|
+
## [1.2.0] - 2024-09-20
|
27
|
+
|
28
|
+
- Fixed bug in CLI where new flags and `--since` would not be recognized
|
29
|
+
- Improved selection of observations included in the output
|
30
|
+
- Fixed calculation of churn that would never be zero
|
19
31
|
- Fixed behavior when --since or short-hand flags were not provided
|
20
32
|
|
21
|
-
## [1.
|
33
|
+
## [1.1.0] - 2024-09-20
|
22
34
|
|
23
|
-
-
|
24
|
-
-
|
25
|
-
|
35
|
+
- Introduced `--summary` flag to output summary statistics for churn and complexity
|
36
|
+
- Introduced `--month`, `--quarter`, and `--year` short-hand flags to calculate churn for different time periods relative to the most recent commit
|
37
|
+
|
38
|
+
## [1.0.0] - 2024-06-07
|
39
|
+
|
40
|
+
- Initial release
|
data/README.md
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
[](https://badge.fury.io/rb/churn_vs_complexity)
|
2
|
+
|
1
3
|
# ChurnVsComplexity
|
2
4
|
|
3
5
|
A tool to visualise code complexity in a project and help direct refactoring efforts.
|
@@ -22,9 +24,11 @@ Or install it yourself as:
|
|
22
24
|
|
23
25
|
$ gem install churn_vs_complexity
|
24
26
|
|
25
|
-
This gem depends on git for churn analysis
|
27
|
+
This gem depends on git for churn analysis.
|
28
|
+
|
29
|
+
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`.
|
26
30
|
|
27
|
-
|
31
|
+
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
32
|
|
29
33
|
## Usage
|
30
34
|
|
@@ -45,10 +49,10 @@ Usage: churn_vs_complexity [options] folder
|
|
45
49
|
-q, --quarter Calculate churn for the quarter leading up to the most recent commit
|
46
50
|
-y, --year Calculate churn for the year leading up to the most recent commit
|
47
51
|
--timetravel N Calculate summary for all commits at intervals of N days throughout project history or from the date specified with --since
|
52
|
+
--delta SHA Identify changes between the specified commit (SHA) and the previous commit and annotate changed files with complexity score. SHA can be a full or short commit hash, or the value HEAD. Can be used multiple times to specify multiple commits.
|
48
53
|
--dry-run Echo the chosen options from the CLI
|
49
54
|
-h, --help Display help
|
50
|
-
|
51
|
-
|
55
|
+
--version Display version
|
52
56
|
```
|
53
57
|
|
54
58
|
Note that when using the `--timetravel` mode, the semantics of some flags are subtly different from normal mode:
|
@@ -71,6 +75,8 @@ Summary points in timetravel mode instead include an alpha score, which is the s
|
|
71
75
|
|
72
76
|
`churn_vs_complexity --java -m --since 2019-03-01 --timetravel 30 --graph my_java_project > ~/Desktop/timetravel-after-1st-march-2019.html`
|
73
77
|
|
78
|
+
`churn_vs_complexity --delta 1496402e81e68e86c5ac240559099fbe581a9a2g --delta 2845296758861773778d70d96328a5f2a1a9e933 --js --summary my_javascript_project > ~/Desktop/interesting-commits.txt`
|
79
|
+
|
74
80
|
## Development
|
75
81
|
|
76
82
|
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
|
-
|
22
|
+
`(#{cmd}) 2>/dev/null`.to_i
|
15
23
|
end
|
16
24
|
|
17
25
|
def date_of_latest_commit(folder:)
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
module ChurnVsComplexity
|
6
|
+
module CLI
|
7
|
+
module Main
|
8
|
+
class << self
|
9
|
+
def run!(options, folder)
|
10
|
+
validate_folder!(folder)
|
11
|
+
validate_options!(options)
|
12
|
+
config = config(options)
|
13
|
+
config.validate!
|
14
|
+
|
15
|
+
config.checker.check(folder:)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def validate_folder!(folder)
|
21
|
+
raise ValidationError, 'No folder selected. Use --help for usage information.' if folder.nil? || folder.empty?
|
22
|
+
raise ValidationError, "Folder #{folder} does not exist" unless File.directory?(folder)
|
23
|
+
end
|
24
|
+
|
25
|
+
def validate_options!(options)
|
26
|
+
raise ValidationError, 'No options selected. Use --help for usage information.' if options.empty?
|
27
|
+
raise ValidationError, 'No language selected. Use --help for usage information.' if options[:language].nil?
|
28
|
+
|
29
|
+
return unless options[:serializer].nil?
|
30
|
+
|
31
|
+
raise ValidationError, 'No serializer selected. Use --help for usage information.'
|
32
|
+
end
|
33
|
+
|
34
|
+
def config(options)
|
35
|
+
config_class =
|
36
|
+
case options[:mode]
|
37
|
+
when :timetravel then Timetravel::Config
|
38
|
+
when :delta then Delta::Config
|
39
|
+
else Normal::Config
|
40
|
+
end
|
41
|
+
config_class.new(**options)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -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 (SHA) and the previous commit and annotate changed files with complexity score. SHA can be a full or short commit hash, or the value HEAD. 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,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
require 'optparse'
|
3
|
+
require_relative 'cli/parser'
|
4
|
+
require_relative 'cli/main'
|
6
5
|
|
7
6
|
module ChurnVsComplexity
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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)
|
88
|
-
|
89
|
-
raise Error, 'No options selected. Use --help for usage information.' if options.empty?
|
90
|
-
|
91
|
-
config = Config.new(**options)
|
92
|
-
|
93
|
-
config.validate!
|
94
|
-
|
95
|
-
if options[:mode] == :timetravel
|
96
|
-
puts config.timetravel.go(folder:)
|
97
|
-
else
|
98
|
-
puts config.to_engine.check(folder:)
|
7
|
+
module CLI
|
8
|
+
class << self
|
9
|
+
def run!
|
10
|
+
parser, options = Parser.create
|
11
|
+
parser.parse!
|
12
|
+
# First argument that is not an option is the folder
|
13
|
+
folder = ARGV.first
|
14
|
+
|
15
|
+
puts Main.run!(options, folder)
|
99
16
|
end
|
100
17
|
end
|
101
18
|
end
|
@@ -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
|
8
|
+
module PMD
|
6
9
|
CONCURRENCY = Etc.nprocessors
|
7
10
|
|
8
11
|
class << self
|
9
|
-
def
|
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
|
-
|
12
|
-
|
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
|
-
|
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
|
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|
|
@@ -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
|
-
|
18
|
-
|
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
|
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,54 @@
|
|
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
|
+
CommitHydrator.new(
|
44
|
+
git_strategy: @factory.git_strategy(folder:),
|
45
|
+
serializer: @serializer,
|
46
|
+
).hydrate(@commit)
|
47
|
+
end
|
48
|
+
|
49
|
+
def valid_commit?(folder:)
|
50
|
+
@factory.git_strategy(folder:).valid_commit?(commit: @commit)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module Delta
|
5
|
+
class CommitHydrator
|
6
|
+
def initialize(git_strategy:, serializer:)
|
7
|
+
@git_strategy = git_strategy
|
8
|
+
@serializer = serializer
|
9
|
+
end
|
10
|
+
|
11
|
+
def hydrate(commit_sha)
|
12
|
+
commit = @git_strategy.object(commit_sha)
|
13
|
+
summary = { commit: commit.sha }
|
14
|
+
if @serializer.respond_to?(:has_commit_summary?) && @serializer.has_commit_summary?
|
15
|
+
parent, next_commit = @git_strategy.surrounding(commit:)
|
16
|
+
summary.merge!(parent:, next_commit:)
|
17
|
+
end
|
18
|
+
summary
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
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
|