churn_vs_complexity 1.8.0 → 1.9.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -0
- data/README.md +1 -1
- data/bin/churn_vs_complexity +6 -1
- data/lib/churn_vs_complexity/churn.rb +17 -2
- data/lib/churn_vs_complexity/complexity/kotlin_calculator.rb +1 -1
- data/lib/churn_vs_complexity/delta/serializer.rb +1 -0
- data/lib/churn_vs_complexity/gate/config.rb +7 -0
- data/lib/churn_vs_complexity/gate/serializer.rb +9 -2
- data/lib/churn_vs_complexity/git_strategy.rb +16 -2
- data/lib/churn_vs_complexity/normal/serializer/summary_hash.rb +21 -8
- data/lib/churn_vs_complexity/timetravel/traveller.rb +2 -0
- data/lib/churn_vs_complexity/version.rb +1 -1
- metadata +6 -43
- data/.devcontainer/devcontainer.json +0 -26
- data/.rubocop.yml +0 -25
- data/CLAUDE.md +0 -60
- data/Dockerfile +0 -34
- data/Dockerfile.dev +0 -27
- data/Rakefile +0 -6
- data/TODO +0 -11
- data/package-lock.json +0 -6
- data/tmp/test-support/delta/ruby-summary.txt +0 -55
- data/tmp/test-support/delta/ruby.csv +0 -12
- data/tmp/test-support/go/main.go +0 -11
- data/tmp/test-support/go/utils.go +0 -13
- data/tmp/test-support/java/small-example/src/main/java/org/example/Main.java +0 -10
- data/tmp/test-support/java/small-example/src/main/java/org/example/spice/Checker.java +0 -28
- data/tmp/test-support/javascript/complex.js +0 -43
- data/tmp/test-support/javascript/moderate.js +0 -12
- data/tmp/test-support/javascript/simple.js +0 -5
- data/tmp/test-support/javascript/typescript-example.ts +0 -26
- data/tmp/test-support/kotlin/Main.kt +0 -7
- data/tmp/test-support/kotlin/Utils.kt +0 -11
- data/tmp/test-support/python/example.py +0 -6
- data/tmp/test-support/python/utils.py +0 -9
- data/tmp/test-support/rust/main.rs +0 -7
- data/tmp/test-support/rust/utils.rs +0 -20
- data/tmp/test-support/swift/main.swift +0 -7
- data/tmp/test-support/swift/utils.swift +0 -24
- data/tmp/test-support/txt/abc.txt +0 -0
- data/tmp/test-support/txt/d.txt +0 -0
- data/tmp/test-support/txt/ef.txt +0 -0
- data/tmp/test-support/txt/ghij.txt +0 -0
- data/tmp/test-support/txt/klm.txt +0 -0
- data/tmp/test-support/txt/nopq.txt +0 -0
- data/tmp/test-support/txt/r.txt +0 -0
- data/tmp/test-support/txt/st.txt +0 -0
- data/tmp/test-support/txt/uvx.txt +0 -0
- data/tmp/test-support/txt/yz.txt +0 -0
- data/tmp/timetravel/.keep +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 612c6e1945c04f5be2c72cdbdfbd4bc91157da01c6d4ab66f5a502a7e109c805
|
|
4
|
+
data.tar.gz: c84417b6b7eeeeb4fac050e32c89f037b010977c342e8db14762d28b2f63d557
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a8d9617c65744a1b04910c0e9206d788cddcc02133820a05d9bad5bdc160d9c11e05552080bee72d2a1a9b53c765e884cd249095e54a866e38cdb6f3b803880e
|
|
7
|
+
data.tar.gz: 7174c9a6d71d2db643b83b72c49973aef249b09aafacc41112e25407b58a3a46675af29f968c5f84226b8e53fb114c3546664f4f9c2b35940277d68d8f9e5fda
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
## [1.9.0] - 2026-09-20
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
- Churn counted only the commits made since the 30th most recent commit. The
|
|
5
|
+
underlying `git` gem caps a log at 30 entries by default, and the clamp that
|
|
6
|
+
is meant to stop `--since` reaching past the first commit was reading that cap
|
|
7
|
+
as the first commit. Any repository with more than 30 commits was undercounted.
|
|
8
|
+
- Timetravel built its commit window from bare dates, which git treats as
|
|
9
|
+
approximate and resolves against the clock and the timezone. Commits on the
|
|
10
|
+
first and last day of the period were included or dropped depending on when
|
|
11
|
+
and where the command ran. Both ends are now given an explicit time.
|
|
12
|
+
- Running any mode against a repository with no commits raised
|
|
13
|
+
`NoMethodError: undefined method 'date' for nil`. It now says which folder has
|
|
14
|
+
no commits.
|
|
15
|
+
- Errors the tool raises on purpose, such as telling you which analyser to
|
|
16
|
+
install, were printed underneath a backtrace. The backtrace is now kept for
|
|
17
|
+
genuinely unexpected failures. These still exit 2, so a gate failure (exit 1)
|
|
18
|
+
stays distinguishable from a tool that could not run.
|
|
19
|
+
- `--gate --max-gamma 0` crashed with `FloatDomainError: Infinity`, because the
|
|
20
|
+
percentage by which a score exceeds the threshold divided by it. Violations are
|
|
21
|
+
now reported against a zero threshold with `exceeds_by` set to null.
|
|
22
|
+
- A negative `--max-gamma` is now rejected instead of reporting every file as a
|
|
23
|
+
violation with a negative excess.
|
|
24
|
+
- Timetravel raised `ArgumentError: invalid slice size` when no commits fell in
|
|
25
|
+
the requested period. An empty period now produces an empty result.
|
|
26
|
+
- The `churn_vs_complexity` executable failed with a `LoadError` when run from
|
|
27
|
+
inside a project that uses Bundler, which is most of the repositories the tool
|
|
28
|
+
exists to analyse. It required `bundler/setup`, so it resolved against the
|
|
29
|
+
analysed project's Gemfile instead of its own installed gems.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- The `git` dependency moves from 2.x to 5.x and `flog` from 4.8 to 4.9.
|
|
33
|
+
- The published gem no longer carries the test fixtures, the Dockerfiles or the
|
|
34
|
+
other development files it had been shipping.
|
|
35
|
+
|
|
36
|
+
### Documentation
|
|
37
|
+
- The README told Go users to install gocyclo. The Go calculator runs gocognit,
|
|
38
|
+
and has since Go support was added.
|
|
39
|
+
|
|
1
40
|
## [1.8.0] - 2026-04-05
|
|
2
41
|
|
|
3
42
|
### Added
|
data/README.md
CHANGED
|
@@ -32,7 +32,7 @@ External tool dependencies per language:
|
|
|
32
32
|
- **Java**: Requires [PMD](https://pmd.github.io) on the search path as `pmd`. On macOS: `brew install pmd`.
|
|
33
33
|
- **JavaScript/TypeScript**: Requires [Node.js](https://nodejs.org) (uses ESLint internally).
|
|
34
34
|
- **Python**: Requires [Radon](https://radon.readthedocs.io) on the search path as `radon`. Install with `pip install radon`.
|
|
35
|
-
- **Go**: Requires [
|
|
35
|
+
- **Go**: Requires [gocognit](https://github.com/uudashr/gocognit) on the search path. Install with `go install github.com/uudashr/gocognit/cmd/gocognit@latest`.
|
|
36
36
|
- **Kotlin**: Requires [lizard](https://github.com/terryyin/lizard) on the search path. Install with `pip install lizard`.
|
|
37
37
|
- **Rust**: Requires [lizard](https://github.com/terryyin/lizard) on the search path. Install with `pip install lizard`.
|
|
38
38
|
- **Swift**: Requires [lizard](https://github.com/terryyin/lizard) on the search path. Install with `pip install lizard`.
|
data/bin/churn_vs_complexity
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require 'bundler/setup'
|
|
5
4
|
require 'churn_vs_complexity'
|
|
6
5
|
|
|
7
6
|
begin
|
|
@@ -9,6 +8,12 @@ begin
|
|
|
9
8
|
rescue ChurnVsComplexity::ValidationError => e
|
|
10
9
|
warn e.message
|
|
11
10
|
exit 1
|
|
11
|
+
rescue ChurnVsComplexity::Error => e
|
|
12
|
+
# Raised deliberately, so the message is the point and a backtrace only
|
|
13
|
+
# buries it. Exit 2, because exit 1 is how the gate reports a codebase that
|
|
14
|
+
# failed its threshold.
|
|
15
|
+
warn e.message
|
|
16
|
+
exit 2
|
|
12
17
|
rescue StandardError => e
|
|
13
18
|
warn e.backtrace
|
|
14
19
|
warn e.message
|
|
@@ -23,13 +23,28 @@ module ChurnVsComplexity
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def date_of_latest_commit(folder:)
|
|
26
|
-
repo(folder).log.first
|
|
26
|
+
latest = repo(folder).log(1).execute.first
|
|
27
|
+
raise Error, "#{folder} has no commits to measure churn against" if latest.nil?
|
|
28
|
+
|
|
29
|
+
latest.date
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
private
|
|
30
33
|
|
|
34
|
+
# Asks git for the root commits directly. Reading it off `repo(folder).log`
|
|
35
|
+
# instead would silently answer with the 30th most recent commit, because
|
|
36
|
+
# that is where the git gem caps a log by default.
|
|
31
37
|
def date_of_first_commit(folder:)
|
|
32
|
-
|
|
38
|
+
git_dir = File.join(folder, '.git')
|
|
39
|
+
cmd = %(git --git-dir #{git_dir} --work-tree #{folder} log --max-parents=0 --format=%ad --date=short)
|
|
40
|
+
dates = `(#{cmd}) 2>/dev/null`.split("\n").filter_map { |line| parse_date(line) }
|
|
41
|
+
dates.min || Time.at(0).to_date
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse_date(line)
|
|
45
|
+
Date.parse(line)
|
|
46
|
+
rescue Date::Error
|
|
47
|
+
nil
|
|
33
48
|
end
|
|
34
49
|
|
|
35
50
|
def repo(folder)
|
|
@@ -16,7 +16,7 @@ module ChurnVsComplexity
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
# Lizard CSV format: NLOC,CCN,tokens,params,length,"loc","file","func","long_func",start,end
|
|
19
|
-
LIZARD_LINE_PATTERN = /^\d+,(\d+),\d+,\d+,\d+,"[^"]*","([^"]*)"
|
|
19
|
+
LIZARD_LINE_PATTERN = /^\d+,(\d+),\d+,\d+,\d+,"[^"]*","([^"]*)"/
|
|
20
20
|
|
|
21
21
|
def parse_lizard_output(csv_output, files:)
|
|
22
22
|
scores = Hash.new(0)
|
|
@@ -29,6 +29,7 @@ module ChurnVsComplexity
|
|
|
29
29
|
LanguageValidator.validate!(@language)
|
|
30
30
|
@since_validator.validate!(since: @since, relative_period: nil)
|
|
31
31
|
@complexity_validator.validate!(@language)
|
|
32
|
+
validate_max_gamma!
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
def checker
|
|
@@ -43,6 +44,12 @@ module ChurnVsComplexity
|
|
|
43
44
|
|
|
44
45
|
private
|
|
45
46
|
|
|
47
|
+
def validate_max_gamma!
|
|
48
|
+
return unless @max_gamma.negative?
|
|
49
|
+
|
|
50
|
+
raise ValidationError, "Max gamma must not be negative, got #{@max_gamma}"
|
|
51
|
+
end
|
|
52
|
+
|
|
46
53
|
def gate_serializer
|
|
47
54
|
case @serializer
|
|
48
55
|
when :json then Serializer::Json
|
|
@@ -24,10 +24,17 @@ module ChurnVsComplexity
|
|
|
24
24
|
gamma = GammaScore.calculate(values[0], values[1]).round(2)
|
|
25
25
|
next unless gamma > max_gamma
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
{ file:, gamma_score: gamma, exceeds_by: "#{exceeds_pct}%" }
|
|
27
|
+
{ file:, gamma_score: gamma, exceeds_by: excess(gamma, max_gamma) }
|
|
29
28
|
end
|
|
30
29
|
end
|
|
30
|
+
|
|
31
|
+
# There is no percentage by which a score exceeds zero, so a zero
|
|
32
|
+
# threshold reports the violation without one.
|
|
33
|
+
def excess(gamma, max_gamma)
|
|
34
|
+
return nil if max_gamma.zero?
|
|
35
|
+
|
|
36
|
+
"#{(((gamma - max_gamma) / max_gamma) * 100).round(0)}%"
|
|
37
|
+
end
|
|
31
38
|
end
|
|
32
39
|
end
|
|
33
40
|
end
|
|
@@ -23,7 +23,7 @@ module ChurnVsComplexity
|
|
|
23
23
|
def surrounding(commit:)
|
|
24
24
|
current = object(commit)
|
|
25
25
|
child_sha = `git -C #{@folder} log --reverse --ancestry-path #{current.sha}..HEAD --format=%H`.lines.first&.chomp
|
|
26
|
-
next_commit = child_sha
|
|
26
|
+
next_commit = child_sha.nil? || child_sha.empty? ? nil : child_sha
|
|
27
27
|
[current.parent&.sha, next_commit]
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -39,8 +39,22 @@ module ChurnVsComplexity
|
|
|
39
39
|
`#{command}`
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Both ends are given an explicit time. A bare date is an approximate date
|
|
43
|
+
# to git, resolved against the clock and the timezone, so the window it
|
|
44
|
+
# produces is not the window that was asked for.
|
|
45
|
+
def commits_in(git_period)
|
|
46
|
+
@repo.log(1_000_000)
|
|
47
|
+
.since(start_of_day(git_period.effective_start_date))
|
|
48
|
+
.until(end_of_day(git_period.end_date))
|
|
49
|
+
.execute.to_a
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def start_of_day(date) = date.strftime('%Y-%m-%dT00:00:00')
|
|
53
|
+
|
|
54
|
+
def end_of_day(date) = date.strftime('%Y-%m-%dT23:59:59')
|
|
55
|
+
|
|
42
56
|
def resolve_commits_with_interval(git_period:, jump_days:)
|
|
43
|
-
candidates =
|
|
57
|
+
candidates = commits_in(git_period)
|
|
44
58
|
|
|
45
59
|
commits_by_date = candidates.filter { |c| c.date.to_date >= git_period.effective_start_date }
|
|
46
60
|
.group_by { |c| c.date.to_date }
|
|
@@ -11,18 +11,31 @@ module ChurnVsComplexity
|
|
|
11
11
|
|
|
12
12
|
return empty_summary(end_date) if values_by_file.empty?
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
complexity_values = values_by_file.map { |_, values| values[1].to_f }
|
|
16
|
-
gamma_scores = values_by_file.map { |_, values| GammaScore.calculate(values[0], values[1]) }
|
|
17
|
-
|
|
18
|
-
stats(churn_values).transform_keys { |k| :"#{k}_churn" }
|
|
19
|
-
.merge(stats(complexity_values).transform_keys { |k| :"#{k}_complexity" })
|
|
20
|
-
.merge(stats(gamma_scores).slice(:mean, :median).transform_keys { |k| :"#{k}_gamma_score" })
|
|
21
|
-
.merge(end_date:)
|
|
14
|
+
summarise(values_by_file).merge(end_date:)
|
|
22
15
|
end
|
|
23
16
|
|
|
24
17
|
private
|
|
25
18
|
|
|
19
|
+
def summarise(values_by_file)
|
|
20
|
+
churn_stats(values_by_file)
|
|
21
|
+
.merge(complexity_stats(values_by_file), gamma_stats(values_by_file))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def churn_stats(values_by_file)
|
|
25
|
+
values = values_by_file.map { |_, pair| pair[0].to_f }
|
|
26
|
+
stats(values).transform_keys { |key| :"#{key}_churn" }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def complexity_stats(values_by_file)
|
|
30
|
+
values = values_by_file.map { |_, pair| pair[1].to_f }
|
|
31
|
+
stats(values).transform_keys { |key| :"#{key}_complexity" }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def gamma_stats(values_by_file)
|
|
35
|
+
values = values_by_file.map { |_, pair| GammaScore.calculate(pair[0], pair[1]) }
|
|
36
|
+
stats(values).slice(:mean, :median).transform_keys { |key| :"#{key}_gamma_score" }
|
|
37
|
+
end
|
|
38
|
+
|
|
26
39
|
def stats(values)
|
|
27
40
|
{ mean: values.sum / values.size, median: values.sort[values.size / 2], max: values.max, min: values.min }
|
|
28
41
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: churn_vs_complexity
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Erik T. Madsen
|
|
@@ -15,28 +15,28 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '4.
|
|
18
|
+
version: '4.9'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '4.
|
|
25
|
+
version: '4.9'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: git
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
32
|
+
version: '5.0'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '
|
|
39
|
+
version: '5.0'
|
|
40
40
|
description: Correlates file churn (how often files change) with complexity scores
|
|
41
41
|
to identify refactoring hotspots. Supports Ruby, JavaScript/TypeScript, Java, Python,
|
|
42
42
|
Go, Kotlin, Rust, and Swift. Modes include hotspots ranking, triage assessment,
|
|
@@ -49,16 +49,9 @@ executables:
|
|
|
49
49
|
extensions: []
|
|
50
50
|
extra_rdoc_files: []
|
|
51
51
|
files:
|
|
52
|
-
- ".devcontainer/devcontainer.json"
|
|
53
|
-
- ".rubocop.yml"
|
|
54
52
|
- CHANGELOG.md
|
|
55
|
-
- CLAUDE.md
|
|
56
|
-
- Dockerfile
|
|
57
|
-
- Dockerfile.dev
|
|
58
53
|
- LICENSE.txt
|
|
59
54
|
- README.md
|
|
60
|
-
- Rakefile
|
|
61
|
-
- TODO
|
|
62
55
|
- bin/churn_vs_complexity
|
|
63
56
|
- lib/churn_vs_complexity.rb
|
|
64
57
|
- lib/churn_vs_complexity/churn.rb
|
|
@@ -132,41 +125,11 @@ files:
|
|
|
132
125
|
- lib/churn_vs_complexity/triage/config.rb
|
|
133
126
|
- lib/churn_vs_complexity/triage/serializer.rb
|
|
134
127
|
- lib/churn_vs_complexity/version.rb
|
|
135
|
-
- package-lock.json
|
|
136
128
|
- tmp/eslint-support/complexity-calculator.js
|
|
137
129
|
- tmp/eslint-support/package.json
|
|
138
130
|
- tmp/pmd-support/ruleset.xml
|
|
139
131
|
- tmp/template/graph.html
|
|
140
132
|
- tmp/template/timetravel_graph.html
|
|
141
|
-
- tmp/test-support/delta/ruby-summary.txt
|
|
142
|
-
- tmp/test-support/delta/ruby.csv
|
|
143
|
-
- tmp/test-support/go/main.go
|
|
144
|
-
- tmp/test-support/go/utils.go
|
|
145
|
-
- tmp/test-support/java/small-example/src/main/java/org/example/Main.java
|
|
146
|
-
- tmp/test-support/java/small-example/src/main/java/org/example/spice/Checker.java
|
|
147
|
-
- tmp/test-support/javascript/complex.js
|
|
148
|
-
- tmp/test-support/javascript/moderate.js
|
|
149
|
-
- tmp/test-support/javascript/simple.js
|
|
150
|
-
- tmp/test-support/javascript/typescript-example.ts
|
|
151
|
-
- tmp/test-support/kotlin/Main.kt
|
|
152
|
-
- tmp/test-support/kotlin/Utils.kt
|
|
153
|
-
- tmp/test-support/python/example.py
|
|
154
|
-
- tmp/test-support/python/utils.py
|
|
155
|
-
- tmp/test-support/rust/main.rs
|
|
156
|
-
- tmp/test-support/rust/utils.rs
|
|
157
|
-
- tmp/test-support/swift/main.swift
|
|
158
|
-
- tmp/test-support/swift/utils.swift
|
|
159
|
-
- tmp/test-support/txt/abc.txt
|
|
160
|
-
- tmp/test-support/txt/d.txt
|
|
161
|
-
- tmp/test-support/txt/ef.txt
|
|
162
|
-
- tmp/test-support/txt/ghij.txt
|
|
163
|
-
- tmp/test-support/txt/klm.txt
|
|
164
|
-
- tmp/test-support/txt/nopq.txt
|
|
165
|
-
- tmp/test-support/txt/r.txt
|
|
166
|
-
- tmp/test-support/txt/st.txt
|
|
167
|
-
- tmp/test-support/txt/uvx.txt
|
|
168
|
-
- tmp/test-support/txt/yz.txt
|
|
169
|
-
- tmp/timetravel/.keep
|
|
170
133
|
homepage: https://github.com/beatmadsen/churn_vs_complexity
|
|
171
134
|
licenses:
|
|
172
135
|
- MIT
|
|
@@ -188,7 +151,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
188
151
|
- !ruby/object:Gem::Version
|
|
189
152
|
version: '0'
|
|
190
153
|
requirements: []
|
|
191
|
-
rubygems_version: 4.0.
|
|
154
|
+
rubygems_version: 4.0.9
|
|
192
155
|
specification_version: 4
|
|
193
156
|
summary: Analyse churn vs complexity to find refactoring hotspots, gate CI quality,
|
|
194
157
|
triage risky files, and track codebase health over time.
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
|
2
|
-
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile
|
|
3
|
-
{
|
|
4
|
-
"name": "Existing Dockerfile",
|
|
5
|
-
"build": {
|
|
6
|
-
// Sets the run context to one level up instead of the .devcontainer folder.
|
|
7
|
-
"context": "..",
|
|
8
|
-
// Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
|
|
9
|
-
"dockerfile": "../Dockerfile.dev"
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Features to add to the dev container. More info: https://containers.dev/features.
|
|
13
|
-
// "features": {},
|
|
14
|
-
|
|
15
|
-
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
|
16
|
-
// "forwardPorts": [],
|
|
17
|
-
|
|
18
|
-
// Uncomment the next line to run commands after the container is created.
|
|
19
|
-
// "postCreateCommand": "cat /etc/os-release",
|
|
20
|
-
|
|
21
|
-
// Configure tool-specific properties.
|
|
22
|
-
// "customizations": {},
|
|
23
|
-
|
|
24
|
-
// Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
|
|
25
|
-
// "remoteUser": "devcontainer"
|
|
26
|
-
}
|
data/.rubocop.yml
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
AllCops:
|
|
2
|
-
TargetRubyVersion: 3.3
|
|
3
|
-
NewCops: enable
|
|
4
|
-
|
|
5
|
-
Layout/LineLength:
|
|
6
|
-
Max: 120
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Style/Documentation:
|
|
10
|
-
Enabled: false
|
|
11
|
-
|
|
12
|
-
Style/ModuleFunction:
|
|
13
|
-
Enabled: false
|
|
14
|
-
|
|
15
|
-
Style/TrailingCommaInArrayLiteral:
|
|
16
|
-
Enabled: true
|
|
17
|
-
EnforcedStyleForMultiline: consistent_comma
|
|
18
|
-
|
|
19
|
-
Style/TrailingCommaInHashLiteral:
|
|
20
|
-
Enabled: true
|
|
21
|
-
EnforcedStyleForMultiline: consistent_comma
|
|
22
|
-
|
|
23
|
-
Style/TrailingCommaInArguments:
|
|
24
|
-
Enabled: true
|
|
25
|
-
EnforcedStyleForMultiline: consistent_comma
|
data/CLAUDE.md
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Project Overview
|
|
6
|
-
|
|
7
|
-
`churn_vs_complexity` is a Ruby gem that analyzes code quality by correlating file churn (how often files change) with complexity scores. It supports Ruby (via Flog), JavaScript/TypeScript (via ESLint), Java (via PMD), Python (via Radon), Go (via gocognit), Kotlin (via lizard), Rust (via lizard), and Swift (via lizard). Requires Ruby >= 3.3.
|
|
8
|
-
|
|
9
|
-
## Commands
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
# Run tests (TLDR framework)
|
|
13
|
-
bundle exec rake # default task runs tldr
|
|
14
|
-
bundle exec tldr # direct
|
|
15
|
-
|
|
16
|
-
# Run a single test file
|
|
17
|
-
bundle exec tldr test/churn_vs_complexity/engine_test.rb
|
|
18
|
-
|
|
19
|
-
# Run a single test by name
|
|
20
|
-
bundle exec tldr --name test_something test/path/to_test.rb
|
|
21
|
-
|
|
22
|
-
# Lint
|
|
23
|
-
bundle exec rubocop
|
|
24
|
-
bundle exec rubocop -a # auto-fix safe offenses
|
|
25
|
-
|
|
26
|
-
# Build/install gem
|
|
27
|
-
bundle exec rake build
|
|
28
|
-
bundle exec rake install
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Architecture
|
|
32
|
-
|
|
33
|
-
Three operating modes, each with its own Config/Checker/Serializer pipeline:
|
|
34
|
-
|
|
35
|
-
- **Normal** (`Normal::Config` → `Engine` → `ConcurrentCalculator`): Analyzes a folder, computing churn + complexity per file. Outputs CSV, HTML graph, or text summary.
|
|
36
|
-
- **Timetravel** (`Timetravel::Config` → `Traveller`): Samples quality scores across historical commits at N-day intervals. Uses forked processes with git worktrees for isolation.
|
|
37
|
-
- **Delta** (`Delta::Config` → `MultiChecker` → `Checker`): Analyzes complexity of files changed in specific commits. Uses threads with git worktrees.
|
|
38
|
-
|
|
39
|
-
Entry point: `CLI::run!` → `CLI::Parser` → `CLI::Main.run!` → mode-specific Config → checker.
|
|
40
|
-
|
|
41
|
-
### Key interfaces
|
|
42
|
-
|
|
43
|
-
- **Complexity calculators**: `folder_based?`, `calculate(folder:)` or `calculate(files:)`
|
|
44
|
-
- **Churn calculators**: `calculate(folder:, file:, since:)`, `date_of_latest_commit(folder:)`
|
|
45
|
-
- **Serializers**: `serialize(result)`
|
|
46
|
-
- **File selectors**: `select_files(folder)` → `{ included: [...], explicitly_excluded: [...] }`
|
|
47
|
-
|
|
48
|
-
### Concurrency model
|
|
49
|
-
|
|
50
|
-
- Normal: threads (nprocessors) for per-file churn calculation
|
|
51
|
-
- Timetravel: forked child processes with IO.pipe for IPC
|
|
52
|
-
- Delta: threads (2x nprocessors) for concurrent commit analysis
|
|
53
|
-
|
|
54
|
-
## Code Style
|
|
55
|
-
|
|
56
|
-
RuboCop config (`.rubocop.yml`):
|
|
57
|
-
- Max line length: 120
|
|
58
|
-
- Trailing commas enforced on multiline (arrays, hashes, arguments)
|
|
59
|
-
- `NewCops: enable`
|
|
60
|
-
- No class/module documentation required (`Style::Documentation` disabled)
|
data/Dockerfile
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# Create a build image with wget and unzip
|
|
2
|
-
FROM ubuntu:latest as pmd_builder
|
|
3
|
-
# Install wget and unzip
|
|
4
|
-
RUN apt-get update && \
|
|
5
|
-
apt-get install -y --no-install-recommends \
|
|
6
|
-
wget unzip
|
|
7
|
-
|
|
8
|
-
RUN wget https://github.com/pmd/pmd/releases/download/pmd_releases%2F7.0.0/pmd-dist-7.0.0-bin.zip --no-check-certificate
|
|
9
|
-
RUN unzip pmd-dist-7.0.0-bin.zip
|
|
10
|
-
|
|
11
|
-
FROM ruby:3.3-bullseye
|
|
12
|
-
# Install git and java JDK
|
|
13
|
-
|
|
14
|
-
RUN apt-get update; \
|
|
15
|
-
apt-get install -y --no-install-recommends \
|
|
16
|
-
git openjdk-17-jdk
|
|
17
|
-
|
|
18
|
-
# copy PMD from build image
|
|
19
|
-
COPY --from=pmd_builder /pmd-bin-7.0.0 /usr/local/bin/pmd-bin-7.0.0
|
|
20
|
-
# make pmd available in the PATH
|
|
21
|
-
RUN ln -s /usr/local/bin/pmd-bin-7.0.0/bin/pmd /usr/local/bin/pmd
|
|
22
|
-
|
|
23
|
-
COPY *.gemspec Gemfile* /app/
|
|
24
|
-
COPY lib /app/lib
|
|
25
|
-
|
|
26
|
-
WORKDIR /app
|
|
27
|
-
|
|
28
|
-
RUN bundle install
|
|
29
|
-
|
|
30
|
-
COPY bin /app/bin
|
|
31
|
-
COPY tmp/pmd-support /app/tmp/pmd-support
|
|
32
|
-
COPY tmp/template /app/tmp/template
|
|
33
|
-
|
|
34
|
-
CMD ["/bin/sh", "-c", "echo 'It works!'"]
|
data/Dockerfile.dev
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# Create a build image with wget and unzip
|
|
2
|
-
FROM ubuntu:latest as pmd_builder
|
|
3
|
-
# Install wget and unzip
|
|
4
|
-
RUN apt-get update && \
|
|
5
|
-
apt-get install -y --no-install-recommends \
|
|
6
|
-
wget unzip
|
|
7
|
-
|
|
8
|
-
RUN wget https://github.com/pmd/pmd/releases/download/pmd_releases%2F7.0.0/pmd-dist-7.0.0-bin.zip --no-check-certificate
|
|
9
|
-
RUN unzip pmd-dist-7.0.0-bin.zip
|
|
10
|
-
|
|
11
|
-
FROM ruby:3.3-bullseye
|
|
12
|
-
# Install git and java JDK
|
|
13
|
-
|
|
14
|
-
RUN apt-get update; \
|
|
15
|
-
apt-get install -y --no-install-recommends \
|
|
16
|
-
git openjdk-17-jdk
|
|
17
|
-
|
|
18
|
-
# copy PMD from build image
|
|
19
|
-
COPY --from=pmd_builder /pmd-bin-7.0.0 /usr/local/bin/pmd-bin-7.0.0
|
|
20
|
-
# make pmd available in the PATH
|
|
21
|
-
RUN ln -s /usr/local/bin/pmd-bin-7.0.0/bin/pmd /usr/local/bin/pmd
|
|
22
|
-
|
|
23
|
-
COPY *.gemspec Gemfile* ./
|
|
24
|
-
COPY lib/churn_vs_complexity/version.rb lib/churn_vs_complexity/version.rb
|
|
25
|
-
RUN bundle install
|
|
26
|
-
|
|
27
|
-
CMD ["/bin/sh", "-c", "echo 'It works!'"]
|
data/Rakefile
DELETED
data/TODO
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
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
|
data/package-lock.json
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
Commit: b20a9bddd04afb9c7a736cf4530fd3188b5f785e
|
|
2
|
-
Parent: 4f3151f83e982eb8f2b8a4e7a7572a0af156c3c0
|
|
3
|
-
Next: 8cdaac516365bd7007b9f755bbe6f6e86d8e13dd
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
File, relative path: lib/churn_vs_complexity/serializer/timetravel.rb
|
|
7
|
-
Type of change: modified
|
|
8
|
-
Complexity: 58.34326171010786
|
|
9
|
-
|
|
10
|
-
File, relative path: lib/churn_vs_complexity/serializer/timetravel/stats_calculator.rb
|
|
11
|
-
Type of change: new
|
|
12
|
-
Complexity: 80.35549194682164
|
|
13
|
-
|
|
14
|
-
File, relative path: test/churn_vs_complexity/serializer/timetravel/quality_calculator_test.rb
|
|
15
|
-
Type of change: modified
|
|
16
|
-
Complexity: 13.054183368177016
|
|
17
|
-
|
|
18
|
-
File, relative path: test/churn_vs_complexity/serializer/timetravel/stats_calculator_test.rb
|
|
19
|
-
Type of change: new
|
|
20
|
-
Complexity: 12.2198021565134
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
Commit: 4f3151f83e982eb8f2b8a4e7a7572a0af156c3c0
|
|
25
|
-
Parent: 83deeccf8aa40e46cd0ca7e2c603c9726633256e
|
|
26
|
-
Next: b20a9bddd04afb9c7a736cf4530fd3188b5f785e
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
File, relative path: lib/churn_vs_complexity/config.rb
|
|
30
|
-
Type of change: modified
|
|
31
|
-
Complexity: 70.26655854867414
|
|
32
|
-
|
|
33
|
-
File, relative path: lib/churn_vs_complexity/serializer/pass_through.rb
|
|
34
|
-
Type of change: new
|
|
35
|
-
Complexity: 19.309088021254183
|
|
36
|
-
|
|
37
|
-
File, relative path: lib/churn_vs_complexity/serializer/summary_hash.rb
|
|
38
|
-
Type of change: modified
|
|
39
|
-
Complexity: 89.25682464940157
|
|
40
|
-
|
|
41
|
-
File, relative path: lib/churn_vs_complexity/serializer/timetravel.rb
|
|
42
|
-
Type of change: modified
|
|
43
|
-
Complexity: 131.0989019732436
|
|
44
|
-
|
|
45
|
-
File, relative path: lib/churn_vs_complexity/serializer/timetravel/quality_calculator.rb
|
|
46
|
-
Type of change: new
|
|
47
|
-
Complexity: 23.324498016752965
|
|
48
|
-
|
|
49
|
-
File, relative path: test/churn_vs_complexity/serializer/summary_hash_test.rb
|
|
50
|
-
Type of change: modified
|
|
51
|
-
Complexity: 14.396336540125482
|
|
52
|
-
|
|
53
|
-
File, relative path: test/churn_vs_complexity/serializer/timetravel/quality_calculator_test.rb
|
|
54
|
-
Type of change: new
|
|
55
|
-
Complexity: 13.054183368177016
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
Commit, Relative Path, Type of Change, Complexity
|
|
2
|
-
b20a9bddd04afb9c7a736cf4530fd3188b5f785e, lib/churn_vs_complexity/serializer/timetravel.rb, modified, 58.34326171010786
|
|
3
|
-
b20a9bddd04afb9c7a736cf4530fd3188b5f785e, lib/churn_vs_complexity/serializer/timetravel/stats_calculator.rb, new, 80.35549194682164
|
|
4
|
-
b20a9bddd04afb9c7a736cf4530fd3188b5f785e, test/churn_vs_complexity/serializer/timetravel/quality_calculator_test.rb, modified, 13.054183368177016
|
|
5
|
-
b20a9bddd04afb9c7a736cf4530fd3188b5f785e, test/churn_vs_complexity/serializer/timetravel/stats_calculator_test.rb, new, 12.2198021565134
|
|
6
|
-
4f3151f83e982eb8f2b8a4e7a7572a0af156c3c0, lib/churn_vs_complexity/config.rb, modified, 70.26655854867414
|
|
7
|
-
4f3151f83e982eb8f2b8a4e7a7572a0af156c3c0, lib/churn_vs_complexity/serializer/pass_through.rb, new, 19.309088021254183
|
|
8
|
-
4f3151f83e982eb8f2b8a4e7a7572a0af156c3c0, lib/churn_vs_complexity/serializer/summary_hash.rb, modified, 89.25682464940157
|
|
9
|
-
4f3151f83e982eb8f2b8a4e7a7572a0af156c3c0, lib/churn_vs_complexity/serializer/timetravel.rb, modified, 131.0989019732436
|
|
10
|
-
4f3151f83e982eb8f2b8a4e7a7572a0af156c3c0, lib/churn_vs_complexity/serializer/timetravel/quality_calculator.rb, new, 23.324498016752965
|
|
11
|
-
4f3151f83e982eb8f2b8a4e7a7572a0af156c3c0, test/churn_vs_complexity/serializer/summary_hash_test.rb, modified, 14.396336540125482
|
|
12
|
-
4f3151f83e982eb8f2b8a4e7a7572a0af156c3c0, test/churn_vs_complexity/serializer/timetravel/quality_calculator_test.rb, new, 13.054183368177016
|
data/tmp/test-support/go/main.go
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
package org.example.spice;
|
|
2
|
-
|
|
3
|
-
import java.util.Iterator;
|
|
4
|
-
import java.util.PrimitiveIterator;
|
|
5
|
-
import java.util.Random;
|
|
6
|
-
|
|
7
|
-
public class Checker {
|
|
8
|
-
public void check() {
|
|
9
|
-
for (int j = 0; j < 3; j++) {
|
|
10
|
-
var iter = tenRandomInts();
|
|
11
|
-
while (iter.hasNext()) {
|
|
12
|
-
var i = iter.nextInt();
|
|
13
|
-
if (i < 100) {
|
|
14
|
-
System.out.println("Bongo");
|
|
15
|
-
} else if (i > 200) {
|
|
16
|
-
System.out.println("Dingo");
|
|
17
|
-
} else {
|
|
18
|
-
System.out.println("Zapp");
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
private static PrimitiveIterator.OfInt tenRandomInts() {
|
|
25
|
-
var randomInts = new Random().ints();
|
|
26
|
-
return randomInts.limit(10).iterator();
|
|
27
|
-
}
|
|
28
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
function analyzeNumber(num) {
|
|
2
|
-
let result = '';
|
|
3
|
-
|
|
4
|
-
if (num < 0) {
|
|
5
|
-
result += 'negative ';
|
|
6
|
-
} else if (num > 0) {
|
|
7
|
-
result += 'positive ';
|
|
8
|
-
} else {
|
|
9
|
-
return 'zero';
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
if (num % 2 === 0) {
|
|
13
|
-
result += 'even ';
|
|
14
|
-
} else {
|
|
15
|
-
result += 'odd ';
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (num % 3 === 0) {
|
|
19
|
-
result += 'divisible by 3 ';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (num % 5 === 0) {
|
|
23
|
-
result += 'divisible by 5 ';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (isPrime(num)) {
|
|
27
|
-
result += 'prime ';
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return result.trim();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function isPrime(num) {
|
|
34
|
-
if (num <= 1) return false;
|
|
35
|
-
for (let i = 2; i <= Math.sqrt(num); i++) {
|
|
36
|
-
if (num % i === 0) return false;
|
|
37
|
-
}
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
console.log(analyzeNumber(17));
|
|
42
|
-
console.log(analyzeNumber(30));
|
|
43
|
-
console.log(analyzeNumber(-7));
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
interface Person {
|
|
2
|
-
name: string;
|
|
3
|
-
age: number;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
function createGreeting(person: Person): string {
|
|
7
|
-
let greeting = `Hello, ${person.name}!`;
|
|
8
|
-
|
|
9
|
-
if (person.age < 18) {
|
|
10
|
-
greeting += " You're still a minor.";
|
|
11
|
-
} else if (person.age >= 18 && person.age < 65) {
|
|
12
|
-
greeting += " You're an adult.";
|
|
13
|
-
} else {
|
|
14
|
-
greeting += " You're a senior citizen.";
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return greeting;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const alice: Person = { name: 'Alice', age: 30 };
|
|
21
|
-
const bob: Person = { name: 'Bob', age: 17 };
|
|
22
|
-
const charlie: Person = { name: 'Charlie', age: 70 };
|
|
23
|
-
|
|
24
|
-
console.log(createGreeting(alice));
|
|
25
|
-
console.log(createGreeting(bob));
|
|
26
|
-
console.log(createGreeting(charlie));
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
fn calculate_sum(numbers: &[i32]) -> i32 {
|
|
2
|
-
let mut total = 0;
|
|
3
|
-
for n in numbers {
|
|
4
|
-
total += n;
|
|
5
|
-
}
|
|
6
|
-
total
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
fn classify(value: i32) -> &'static str {
|
|
10
|
-
match value {
|
|
11
|
-
0 => "zero",
|
|
12
|
-
1..=10 => "low",
|
|
13
|
-
11..=100 => "medium",
|
|
14
|
-
_ => "high",
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
fn is_even(n: i32) -> bool {
|
|
19
|
-
n % 2 == 0
|
|
20
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
func calculateSum(_ numbers: [Int]) -> Int {
|
|
2
|
-
var total = 0
|
|
3
|
-
for n in numbers {
|
|
4
|
-
total += n
|
|
5
|
-
}
|
|
6
|
-
return total
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
func classify(_ value: Int) -> String {
|
|
10
|
-
switch value {
|
|
11
|
-
case 0:
|
|
12
|
-
return "zero"
|
|
13
|
-
case 1...10:
|
|
14
|
-
return "low"
|
|
15
|
-
case 11...100:
|
|
16
|
-
return "medium"
|
|
17
|
-
default:
|
|
18
|
-
return "high"
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
func isEven(_ n: Int) -> Bool {
|
|
23
|
-
return n % 2 == 0
|
|
24
|
-
}
|
|
File without changes
|
data/tmp/test-support/txt/d.txt
DELETED
|
File without changes
|
data/tmp/test-support/txt/ef.txt
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
data/tmp/test-support/txt/r.txt
DELETED
|
File without changes
|
data/tmp/test-support/txt/st.txt
DELETED
|
File without changes
|
|
File without changes
|
data/tmp/test-support/txt/yz.txt
DELETED
|
File without changes
|
data/tmp/timetravel/.keep
DELETED
|
File without changes
|