stud-finder 0.1.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 +7 -0
- data/CHANGELOG.md +17 -0
- data/LICENSE +21 -0
- data/PRODUCT.md +172 -0
- data/README.md +176 -0
- data/VISION.md +151 -0
- data/bin/stud-finder +6 -0
- data/lib/stud-finder.rb +3 -0
- data/lib/stud_finder/churn.rb +111 -0
- data/lib/stud_finder/cli.rb +771 -0
- data/lib/stud_finder/complexity.rb +104 -0
- data/lib/stud_finder/coverage/cobertura.rb +59 -0
- data/lib/stud_finder/coverage/detector.rb +26 -0
- data/lib/stud_finder/coverage/lcov.rb +93 -0
- data/lib/stud_finder/coverage/resultset.rb +103 -0
- data/lib/stud_finder/diff.rb +53 -0
- data/lib/stud_finder/edges.rb +113 -0
- data/lib/stud_finder/fan_in.rb +243 -0
- data/lib/stud_finder/file_collector.rb +152 -0
- data/lib/stud_finder/js_complexity.rb +203 -0
- data/lib/stud_finder/js_fan_in.rb +121 -0
- data/lib/stud_finder/normalizer.rb +38 -0
- data/lib/stud_finder/scorer.rb +171 -0
- data/lib/stud_finder/temporal_coupling.rb +104 -0
- data/lib/stud_finder/version.rb +5 -0
- data/lib/stud_finder.rb +14 -0
- metadata +199 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module StudFinder
|
|
6
|
+
class Churn
|
|
7
|
+
class Result
|
|
8
|
+
attr_reader :churn_commits, :churn_lines, :zero_inflated, :zero_percentage
|
|
9
|
+
|
|
10
|
+
def initialize(zero_inflated:, zero_percentage:, **values)
|
|
11
|
+
@churn_commits = values[:churn_commits] || values[:counts] || {}
|
|
12
|
+
@churn_lines = values[:churn_lines] || values[:line_counts] || {}
|
|
13
|
+
@zero_inflated = zero_inflated
|
|
14
|
+
@zero_percentage = zero_percentage
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def counts
|
|
18
|
+
churn_commits
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def line_counts
|
|
22
|
+
churn_lines
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class Error < StandardError; end
|
|
27
|
+
|
|
28
|
+
def initialize(repo_path:, files:, days:, stderr: $stderr)
|
|
29
|
+
@repo_path = File.expand_path(repo_path)
|
|
30
|
+
@files = files
|
|
31
|
+
@days = days
|
|
32
|
+
@stderr = stderr
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def call
|
|
36
|
+
stdout, _stderr, status = git_log
|
|
37
|
+
raise Error, "Error: #{@repo_path} is not a git repository." unless status.success?
|
|
38
|
+
|
|
39
|
+
counts = initial_counts
|
|
40
|
+
file_set = counts.keys.to_h { |file| [file, true] }
|
|
41
|
+
line_counts = initial_counts
|
|
42
|
+
stdout.each_line do |line|
|
|
43
|
+
line = line.strip
|
|
44
|
+
next if line.empty?
|
|
45
|
+
|
|
46
|
+
added, deleted, path = line.split("\t", 3)
|
|
47
|
+
next if path.nil?
|
|
48
|
+
|
|
49
|
+
relative = normalize_path(path)
|
|
50
|
+
next unless file_set[relative]
|
|
51
|
+
|
|
52
|
+
counts[relative] += 1
|
|
53
|
+
line_counts[relative] += added.to_i + deleted.to_i if added != '-' && numeric?(deleted)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
Result.new(
|
|
57
|
+
churn_commits: counts,
|
|
58
|
+
churn_lines: line_counts,
|
|
59
|
+
zero_inflated: zero_inflated?(counts),
|
|
60
|
+
zero_percentage: zero_percentage(counts)
|
|
61
|
+
).tap { |result| warn_if_zero_inflated(result) }
|
|
62
|
+
rescue Errno::ENOENT
|
|
63
|
+
raise Error, 'Error: git not found in PATH.'
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def git_log
|
|
69
|
+
Open3.capture3(
|
|
70
|
+
'git', '-C', @repo_path, 'log',
|
|
71
|
+
"--since=#{@days} days ago",
|
|
72
|
+
'--format=tformat:',
|
|
73
|
+
'--numstat',
|
|
74
|
+
'--no-merges',
|
|
75
|
+
'--diff-filter=ACDMR'
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def initial_counts
|
|
80
|
+
@files.to_h { |file| [file, 0] }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def normalize_path(path)
|
|
84
|
+
absolute = File.expand_path(path, @repo_path)
|
|
85
|
+
absolute.start_with?("#{@repo_path}/") ? absolute.delete_prefix("#{@repo_path}/") : path
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def numeric?(value)
|
|
89
|
+
value&.match?(/\A\d+\z/)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def zero_inflated?(counts)
|
|
93
|
+
return false if counts.empty?
|
|
94
|
+
|
|
95
|
+
counts.values.count(&:zero?) > counts.length * 0.5
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def zero_percentage(counts)
|
|
99
|
+
return 0 if counts.empty?
|
|
100
|
+
|
|
101
|
+
((counts.values.count(&:zero?).to_f / counts.length) * 100).round
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def warn_if_zero_inflated(result)
|
|
105
|
+
return unless result.zero_inflated
|
|
106
|
+
|
|
107
|
+
@stderr.puts "Warning: #{result.zero_percentage}% of files have zero churn in the last #{@days} days. " \
|
|
108
|
+
'Churn signal is weak. Consider --churn-days to widen the window.'
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|