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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +24 -14
- data/README.md +7 -4
- data/lib/churn_vs_complexity/churn.rb +9 -1
- data/lib/churn_vs_complexity/cli/parser.rb +91 -0
- data/lib/churn_vs_complexity/cli.rb +32 -90
- 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 +56 -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 +51 -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 +60 -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 +36 -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: dba0d67852cef763abe50d7fa5a63a7e9b4687b5232d05b281ed267cf948a0c4
|
4
|
+
data.tar.gz: 1839953429d9840ce96782431d530f45e8169441e54c94badf9f12fc9aea7b0a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 699fad42098c2a7e9e4e1368a03fda265ef3333b0e9c0895f6095e38898cf68a36a1c15d152de98f39a4f2a3ce866ce481bea68f83033876aa0d400cfe024b57
|
7
|
+
data.tar.gz: dd4a1412a7c49e76c7dd7624e80f5bd06f3c5bb027402f3eba8f9842568db3f4f6a2e1f6b9b5a559af6eee61c3049e7cd8848640432e3929f4b554b9c28dc847
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,25 +1,35 @@
|
|
1
|
-
##
|
1
|
+
## [1.5.1] - 2024-10-15
|
2
2
|
|
3
|
-
-
|
3
|
+
- Fix bug where worktree checkout silently failed
|
4
4
|
|
5
|
-
## [1.1.0] - 2024-09-20
|
6
5
|
|
7
|
-
|
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
|
-
|
8
|
+
- Added delta mode to annotate changes in individual commits with complexity
|
9
|
+
- Moving PMD cache to the OS temp directory
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
-
|
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
|
-
-
|
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.
|
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
|
-
-
|
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
|
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
|
-
|
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
|
-
|
4
|
-
require 'time'
|
5
|
-
require 'optparse'
|
3
|
+
require_relative 'cli/parser'
|
6
4
|
|
7
5
|
module ChurnVsComplexity
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
21
|
+
private
|
90
22
|
|
91
|
-
|
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
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
@@ -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,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
|