ruby-maat 1.0.0 → 1.2.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/.commitlintrc.json +2 -1
- data/.release-please-manifest.json +1 -1
- data/.rubocop.yml +23 -1
- data/.standard.yml +3 -0
- data/CHANGELOG.md +100 -0
- data/CLAUDE.md +100 -50
- data/README.md +99 -8
- data/lib/ruby_maat/analysis/churn.rb +2 -2
- data/lib/ruby_maat/analysis/effort.rb +5 -5
- data/lib/ruby_maat/analysis_presets.rb +313 -0
- data/lib/ruby_maat/cli.rb +253 -5
- data/lib/ruby_maat/generators/base_generator.rb +267 -0
- data/lib/ruby_maat/generators/git_generator.rb +176 -0
- data/lib/ruby_maat/generators/svn_generator.rb +201 -0
- data/lib/ruby_maat/parsers/git2_parser.rb +2 -2
- data/lib/ruby_maat/parsers/git_parser.rb +2 -2
- data/lib/ruby_maat/parsers/mercurial_parser.rb +1 -1
- data/lib/ruby_maat/parsers/perforce_parser.rb +2 -2
- data/lib/ruby_maat/parsers/tfs_parser.rb +3 -3
- data/lib/ruby_maat/vcs_detector.rb +75 -0
- data/lib/ruby_maat/version.rb +1 -1
- data/release-please-config.json +8 -0
- metadata +9 -4
- data/.release-please-config.json +0 -33
- data/RELEASE_PLEASE_SETUP.md +0 -198
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module RubyMaat
|
|
6
|
+
module AnalysisPresets
|
|
7
|
+
# Analysis configurations with appropriate time ranges and descriptions
|
|
8
|
+
ANALYSIS_CONFIGS = {
|
|
9
|
+
"authors" => {
|
|
10
|
+
name: "Developer Activity Analysis",
|
|
11
|
+
description: "Shows number of developers working on each module",
|
|
12
|
+
time_sensitive: true,
|
|
13
|
+
presets: {
|
|
14
|
+
"team-activity" => {
|
|
15
|
+
description: "Team activity patterns (6 months)",
|
|
16
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
17
|
+
},
|
|
18
|
+
"recent-team" => {
|
|
19
|
+
description: "Recent team changes (3 months)",
|
|
20
|
+
since: -> { (Date.today - 90).strftime("%Y-%m-%d") }
|
|
21
|
+
},
|
|
22
|
+
"team-history" => {
|
|
23
|
+
description: "Complete team evolution (2 years)",
|
|
24
|
+
since: -> { (Date.today - 730).strftime("%Y-%m-%d") }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
"coupling" => {
|
|
30
|
+
name: "Logical Coupling Analysis",
|
|
31
|
+
description: "Finds modules that change together (hidden dependencies)",
|
|
32
|
+
time_sensitive: true,
|
|
33
|
+
presets: {
|
|
34
|
+
"recent-coupling" => {
|
|
35
|
+
description: "Current coupling patterns (3 months)",
|
|
36
|
+
since: -> { (Date.today - 90).strftime("%Y-%m-%d") }
|
|
37
|
+
},
|
|
38
|
+
"coupling-trends" => {
|
|
39
|
+
description: "Coupling evolution (1 year)",
|
|
40
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
41
|
+
},
|
|
42
|
+
"architecture-review" => {
|
|
43
|
+
description: "Deep architecture analysis (6 months)",
|
|
44
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
"age" => {
|
|
50
|
+
name: "Code Age Analysis",
|
|
51
|
+
description: "Shows how recently each module was modified",
|
|
52
|
+
time_sensitive: false,
|
|
53
|
+
presets: {
|
|
54
|
+
"full-history" => {
|
|
55
|
+
description: "Complete age analysis (all history)"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
"abs-churn" => {
|
|
61
|
+
name: "Absolute Code Churn",
|
|
62
|
+
description: "Total lines added/deleted over time",
|
|
63
|
+
time_sensitive: true,
|
|
64
|
+
presets: {
|
|
65
|
+
"recent-churn" => {
|
|
66
|
+
description: "Recent development activity (6 months)",
|
|
67
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
68
|
+
},
|
|
69
|
+
"yearly-churn" => {
|
|
70
|
+
description: "Annual churn patterns (1 year)",
|
|
71
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
72
|
+
},
|
|
73
|
+
"project-churn" => {
|
|
74
|
+
description: "Project lifecycle churn (2 years)",
|
|
75
|
+
since: -> { (Date.today - 730).strftime("%Y-%m-%d") }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
"author-churn" => {
|
|
81
|
+
name: "Author Churn Analysis",
|
|
82
|
+
description: "Code churn by individual developers",
|
|
83
|
+
time_sensitive: true,
|
|
84
|
+
presets: {
|
|
85
|
+
"contributor-activity" => {
|
|
86
|
+
description: "Current contributor patterns (6 months)",
|
|
87
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
88
|
+
},
|
|
89
|
+
"team-contributions" => {
|
|
90
|
+
description: "Team contribution history (1 year)",
|
|
91
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
"entity-churn" => {
|
|
97
|
+
name: "Module Churn Analysis",
|
|
98
|
+
description: "Churn by individual modules/files",
|
|
99
|
+
time_sensitive: true,
|
|
100
|
+
presets: {
|
|
101
|
+
"hotspot-analysis" => {
|
|
102
|
+
description: "Current hotspots (6 months)",
|
|
103
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
104
|
+
},
|
|
105
|
+
"stability-review" => {
|
|
106
|
+
description: "Module stability patterns (1 year)",
|
|
107
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
"entity-ownership" => {
|
|
113
|
+
name: "Code Ownership Analysis",
|
|
114
|
+
description: "Shows ownership distribution of code",
|
|
115
|
+
time_sensitive: true,
|
|
116
|
+
presets: {
|
|
117
|
+
"current-ownership" => {
|
|
118
|
+
description: "Current ownership patterns (1 year)",
|
|
119
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
120
|
+
},
|
|
121
|
+
"ownership-evolution" => {
|
|
122
|
+
description: "Ownership changes over time (2 years)",
|
|
123
|
+
since: -> { (Date.today - 730).strftime("%Y-%m-%d") }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
"main-dev" => {
|
|
129
|
+
name: "Main Developer Analysis",
|
|
130
|
+
description: "Identifies primary developer for each module",
|
|
131
|
+
time_sensitive: true,
|
|
132
|
+
presets: {
|
|
133
|
+
"current-maintainers" => {
|
|
134
|
+
description: "Current module maintainers (1 year)",
|
|
135
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
136
|
+
},
|
|
137
|
+
"maintainer-history" => {
|
|
138
|
+
description: "Maintainer evolution (2 years)",
|
|
139
|
+
since: -> { (Date.today - 730).strftime("%Y-%m-%d") }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
"entity-effort" => {
|
|
145
|
+
name: "Development Effort Analysis",
|
|
146
|
+
description: "Effort distribution across modules",
|
|
147
|
+
time_sensitive: true,
|
|
148
|
+
presets: {
|
|
149
|
+
"effort-focus" => {
|
|
150
|
+
description: "Recent effort distribution (6 months)",
|
|
151
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
152
|
+
},
|
|
153
|
+
"effort-trends" => {
|
|
154
|
+
description: "Effort patterns over time (1 year)",
|
|
155
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
"communication" => {
|
|
161
|
+
name: "Team Communication Analysis",
|
|
162
|
+
description: "Developer collaboration patterns",
|
|
163
|
+
time_sensitive: true,
|
|
164
|
+
presets: {
|
|
165
|
+
"team-collaboration" => {
|
|
166
|
+
description: "Current collaboration patterns (6 months)",
|
|
167
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
168
|
+
},
|
|
169
|
+
"communication-trends" => {
|
|
170
|
+
description: "Communication evolution (1 year)",
|
|
171
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
"summary" => {
|
|
177
|
+
name: "Project Summary",
|
|
178
|
+
description: "High-level project statistics",
|
|
179
|
+
time_sensitive: true,
|
|
180
|
+
presets: {
|
|
181
|
+
"project-overview" => {
|
|
182
|
+
description: "Current project state (1 year)",
|
|
183
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
184
|
+
},
|
|
185
|
+
"full-summary" => {
|
|
186
|
+
description: "Complete project history"
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
"revisions" => {
|
|
192
|
+
name: "Revision Count Analysis",
|
|
193
|
+
description: "Number of changes per module",
|
|
194
|
+
time_sensitive: true,
|
|
195
|
+
presets: {
|
|
196
|
+
"activity-hotspots" => {
|
|
197
|
+
description: "Current activity patterns (6 months)",
|
|
198
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
199
|
+
},
|
|
200
|
+
"change-history" => {
|
|
201
|
+
description: "Complete change patterns (2 years)",
|
|
202
|
+
since: -> { (Date.today - 730).strftime("%Y-%m-%d") }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
# Analyses that don't benefit from time filtering
|
|
208
|
+
"identity" => {
|
|
209
|
+
name: "Identity Analysis",
|
|
210
|
+
description: "Raw parsed data (debugging/export)",
|
|
211
|
+
time_sensitive: false,
|
|
212
|
+
presets: {
|
|
213
|
+
"full-data" => {
|
|
214
|
+
description: "Complete dataset export"
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
"soc" => {
|
|
220
|
+
name: "Sum of Coupling",
|
|
221
|
+
description: "Total coupling strength per module",
|
|
222
|
+
time_sensitive: true,
|
|
223
|
+
presets: {
|
|
224
|
+
"coupling-strength" => {
|
|
225
|
+
description: "Current coupling analysis (6 months)",
|
|
226
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
"refactoring-main-dev" => {
|
|
232
|
+
name: "Refactoring Main Developer",
|
|
233
|
+
description: "Main developer in refactoring contexts",
|
|
234
|
+
time_sensitive: true,
|
|
235
|
+
presets: {
|
|
236
|
+
"refactoring-leads" => {
|
|
237
|
+
description: "Recent refactoring activity (1 year)",
|
|
238
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
"main-dev-by-revs" => {
|
|
244
|
+
name: "Main Developer (by Revisions)",
|
|
245
|
+
description: "Main developer by revision count",
|
|
246
|
+
time_sensitive: true,
|
|
247
|
+
presets: {
|
|
248
|
+
"revision-leaders" => {
|
|
249
|
+
description: "Current revision leaders (1 year)",
|
|
250
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
"fragmentation" => {
|
|
256
|
+
name: "Development Fragmentation",
|
|
257
|
+
description: "How fragmented development effort is",
|
|
258
|
+
time_sensitive: true,
|
|
259
|
+
presets: {
|
|
260
|
+
"team-fragmentation" => {
|
|
261
|
+
description: "Current team fragmentation (6 months)",
|
|
262
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
"messages" => {
|
|
268
|
+
name: "Commit Message Analysis",
|
|
269
|
+
description: "Analyzes commit message patterns",
|
|
270
|
+
time_sensitive: true,
|
|
271
|
+
presets: {
|
|
272
|
+
"message-patterns" => {
|
|
273
|
+
description: "Recent commit patterns (6 months)",
|
|
274
|
+
since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
|
|
275
|
+
},
|
|
276
|
+
"communication-style" => {
|
|
277
|
+
description: "Team communication style (1 year)",
|
|
278
|
+
since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}.freeze
|
|
283
|
+
|
|
284
|
+
def self.analysis_config(analysis_name)
|
|
285
|
+
ANALYSIS_CONFIGS[analysis_name]
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def self.available_analyses
|
|
289
|
+
ANALYSIS_CONFIGS.keys.sort
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def self.analysis_description(analysis_name)
|
|
293
|
+
config = ANALYSIS_CONFIGS[analysis_name]
|
|
294
|
+
return "Unknown analysis" unless config
|
|
295
|
+
"#{config[:name]} - #{config[:description]}"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def self.presets_for_analysis(analysis_name)
|
|
299
|
+
config = ANALYSIS_CONFIGS[analysis_name]
|
|
300
|
+
return {} unless config
|
|
301
|
+
|
|
302
|
+
# Evaluate lambda functions in preset options
|
|
303
|
+
config[:presets].transform_values do |preset|
|
|
304
|
+
preset.transform_values { |v| v.is_a?(Proc) ? v.call : v }
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def self.time_sensitive?(analysis_name)
|
|
309
|
+
config = ANALYSIS_CONFIGS[analysis_name]
|
|
310
|
+
config ? config[:time_sensitive] : false
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
data/lib/ruby_maat/cli.rb
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require "optparse"
|
|
4
4
|
require "date"
|
|
5
|
+
require_relative "generators/git_generator"
|
|
6
|
+
require_relative "generators/svn_generator"
|
|
7
|
+
require_relative "analysis_presets"
|
|
8
|
+
require_relative "vcs_detector"
|
|
5
9
|
|
|
6
10
|
module RubyMaat
|
|
7
11
|
# Command Line Interface - Ruby port of code-maat.cmd-line
|
|
@@ -23,8 +27,12 @@ module RubyMaat
|
|
|
23
27
|
|
|
24
28
|
validate_required_options!
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
if @options[:generate_log] || @options[:interactive]
|
|
31
|
+
handle_log_generation
|
|
32
|
+
else
|
|
33
|
+
app = App.new(@options)
|
|
34
|
+
app.run
|
|
35
|
+
end
|
|
28
36
|
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
|
29
37
|
warn "Error: #{e.message}"
|
|
30
38
|
warn usage
|
|
@@ -41,6 +49,212 @@ module RubyMaat
|
|
|
41
49
|
|
|
42
50
|
private
|
|
43
51
|
|
|
52
|
+
def handle_log_generation
|
|
53
|
+
if @options[:interactive]
|
|
54
|
+
handle_interactive_mode
|
|
55
|
+
else
|
|
56
|
+
generator = create_log_generator
|
|
57
|
+
output_file = @options[:save_log]
|
|
58
|
+
preset_options = get_preset_options if @options[:preset]
|
|
59
|
+
|
|
60
|
+
log_output = generator.generate_log(output_file, **(preset_options || {}))
|
|
61
|
+
|
|
62
|
+
if output_file
|
|
63
|
+
puts "Log generated: #{output_file}"
|
|
64
|
+
# If we saved to file and analysis is specified, run analysis on that file
|
|
65
|
+
if @options[:analysis] && @options[:analysis] != "authors"
|
|
66
|
+
puts "\n=== Running Analysis ==="
|
|
67
|
+
analysis_options = @options.merge(log: output_file)
|
|
68
|
+
app = App.new(analysis_options)
|
|
69
|
+
app.run
|
|
70
|
+
end
|
|
71
|
+
elsif log_output
|
|
72
|
+
# If no output file specified and we have an analysis, run it on the generated log
|
|
73
|
+
puts "\n=== Running Analysis ==="
|
|
74
|
+
|
|
75
|
+
require "tempfile"
|
|
76
|
+
temp_log = Tempfile.new(["ruby_maat", ".log"])
|
|
77
|
+
temp_log.write(log_output)
|
|
78
|
+
temp_log.close
|
|
79
|
+
|
|
80
|
+
analysis_options = @options.merge(log: temp_log.path)
|
|
81
|
+
app = App.new(analysis_options)
|
|
82
|
+
app.run
|
|
83
|
+
|
|
84
|
+
temp_log.unlink
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def handle_interactive_mode
|
|
90
|
+
unless $stdin.tty?
|
|
91
|
+
raise "Interactive mode requires a terminal (TTY). Use --generate-log with presets instead."
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
puts "=== Ruby Maat Interactive Mode ==="
|
|
95
|
+
puts
|
|
96
|
+
|
|
97
|
+
# Step 1: Detect or choose VCS
|
|
98
|
+
vcs_type = @options[:version_control] || detect_vcs_interactive
|
|
99
|
+
|
|
100
|
+
# Step 2: Choose analysis type
|
|
101
|
+
analysis_type = @options[:analysis] || choose_analysis_interactive
|
|
102
|
+
|
|
103
|
+
# Step 3: Generate log and run analysis
|
|
104
|
+
generator = create_log_generator_for_vcs(vcs_type)
|
|
105
|
+
log_output = generator.interactive_generate_for_analysis(analysis_type, @options)
|
|
106
|
+
|
|
107
|
+
# Step 4: Run analysis if log was generated to stdout
|
|
108
|
+
if log_output && !@options[:save_log]
|
|
109
|
+
puts "\n=== Running Analysis ==="
|
|
110
|
+
|
|
111
|
+
# Create temporary log file for analysis
|
|
112
|
+
require "tempfile"
|
|
113
|
+
temp_log = Tempfile.new(["ruby_maat", ".log"])
|
|
114
|
+
temp_log.write(log_output)
|
|
115
|
+
temp_log.close
|
|
116
|
+
|
|
117
|
+
# Run analysis
|
|
118
|
+
analysis_options = @options.merge(
|
|
119
|
+
log: temp_log.path,
|
|
120
|
+
version_control: (vcs_type == "git") ? "git2" : vcs_type,
|
|
121
|
+
analysis: analysis_type
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
app = App.new(analysis_options)
|
|
125
|
+
app.run
|
|
126
|
+
|
|
127
|
+
temp_log.unlink
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def create_log_generator
|
|
132
|
+
case @options[:version_control]
|
|
133
|
+
when "git", "git2"
|
|
134
|
+
RubyMaat::Generators::GitGenerator.new(".", @options)
|
|
135
|
+
when "svn"
|
|
136
|
+
RubyMaat::Generators::SvnGenerator.new(".", @options)
|
|
137
|
+
else
|
|
138
|
+
raise ArgumentError, "Log generation not yet supported for #{@options[:version_control]}"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def get_preset_options
|
|
143
|
+
generator = create_log_generator
|
|
144
|
+
presets = generator.available_presets
|
|
145
|
+
|
|
146
|
+
unless presets.key?(@options[:preset])
|
|
147
|
+
available = presets.keys.join(", ")
|
|
148
|
+
raise ArgumentError, "Unknown preset '#{@options[:preset]}'. Available: #{available}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
presets[@options[:preset]][:options]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def create_log_generator_for_vcs(vcs_type)
|
|
155
|
+
case vcs_type
|
|
156
|
+
when "git", "git2"
|
|
157
|
+
RubyMaat::Generators::GitGenerator.new(".", @options)
|
|
158
|
+
when "svn"
|
|
159
|
+
RubyMaat::Generators::SvnGenerator.new(".", @options)
|
|
160
|
+
else
|
|
161
|
+
raise ArgumentError, "Log generation not yet supported for #{vcs_type}"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def detect_vcs_interactive
|
|
166
|
+
detected = RubyMaat::VcsDetector.detect_vcs
|
|
167
|
+
|
|
168
|
+
if detected
|
|
169
|
+
puts "Detected VCS: #{RubyMaat::VcsDetector.vcs_description(detected)}"
|
|
170
|
+
if ask_yes_no_interactive("Use detected VCS?", true)
|
|
171
|
+
return detected
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
choose_vcs_interactive
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def choose_vcs_interactive
|
|
179
|
+
puts "Choose version control system:"
|
|
180
|
+
vcs_options = %w[git svn hg p4 tfs]
|
|
181
|
+
vcs_options.each_with_index do |vcs, index|
|
|
182
|
+
puts " #{index + 1}. #{RubyMaat::VcsDetector.vcs_description(vcs)}"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
choice = ask_integer_interactive("Choose VCS", 1, vcs_options.length)
|
|
186
|
+
vcs_options[choice - 1]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def choose_analysis_interactive
|
|
190
|
+
analyses = RubyMaat::AnalysisPresets.available_analyses
|
|
191
|
+
|
|
192
|
+
puts "Choose analysis type:"
|
|
193
|
+
analyses.each_with_index do |analysis, index|
|
|
194
|
+
puts " #{index + 1}. #{RubyMaat::AnalysisPresets.analysis_description(analysis)}"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
choice = ask_integer_interactive("Choose analysis", 1, analyses.length)
|
|
198
|
+
analyses[choice - 1]
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def ask_yes_no_interactive(prompt, default = nil)
|
|
202
|
+
default_text = case default
|
|
203
|
+
when true then " [Y/n]"
|
|
204
|
+
when false then " [y/N]"
|
|
205
|
+
else " [y/n]"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
loop do
|
|
209
|
+
print "#{prompt}#{default_text}: "
|
|
210
|
+
response = $stdin.gets
|
|
211
|
+
return default if response.nil?
|
|
212
|
+
response = response.chomp.downcase
|
|
213
|
+
|
|
214
|
+
case response
|
|
215
|
+
when "y", "yes"
|
|
216
|
+
return true
|
|
217
|
+
when "n", "no"
|
|
218
|
+
return false
|
|
219
|
+
when ""
|
|
220
|
+
return default unless default.nil?
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
puts "Please enter 'y' or 'n'"
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def ask_integer_interactive(prompt, min = nil, max = nil)
|
|
228
|
+
attempts = 0
|
|
229
|
+
max_attempts = 10
|
|
230
|
+
|
|
231
|
+
loop do
|
|
232
|
+
attempts += 1
|
|
233
|
+
if attempts > max_attempts
|
|
234
|
+
raise "Too many invalid attempts. Exiting interactive mode."
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
print "#{prompt}: "
|
|
238
|
+
response = $stdin.gets
|
|
239
|
+
return 1 if response.nil? # Default to first option
|
|
240
|
+
|
|
241
|
+
response = response.chomp
|
|
242
|
+
|
|
243
|
+
if response.empty?
|
|
244
|
+
puts "Please enter a valid number"
|
|
245
|
+
next
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
begin
|
|
249
|
+
value = Integer(response)
|
|
250
|
+
return value if (min.nil? || value >= min) && (max.nil? || value <= max)
|
|
251
|
+
puts "Please enter a number between #{min} and #{max}"
|
|
252
|
+
rescue ArgumentError
|
|
253
|
+
puts "Please enter a valid number"
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
44
258
|
def build_option_parser
|
|
45
259
|
OptionParser.new do |opts|
|
|
46
260
|
opts.banner = usage_banner
|
|
@@ -113,6 +327,23 @@ module RubyMaat
|
|
|
113
327
|
@options[:max_changeset_size] = max_size
|
|
114
328
|
end
|
|
115
329
|
|
|
330
|
+
# Log generation options
|
|
331
|
+
opts.on("--generate-log", "Generate log file instead of running analysis") do
|
|
332
|
+
@options[:generate_log] = true
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
opts.on("--save-log FILENAME", "Save generated log to file") do |filename|
|
|
336
|
+
@options[:save_log] = filename
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
opts.on("--interactive", "Use interactive mode for log generation") do
|
|
340
|
+
@options[:interactive] = true
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
opts.on("--preset PRESET", "Use a preset configuration for log generation") do |preset|
|
|
344
|
+
@options[:preset] = preset
|
|
345
|
+
end
|
|
346
|
+
|
|
116
347
|
# Analysis-specific options
|
|
117
348
|
opts.on("-e", "--expression-to-match MATCH_EXPRESSION",
|
|
118
349
|
"A regex to match against commit messages. Used with -messages analyses") do |expression|
|
|
@@ -158,7 +389,10 @@ module RubyMaat
|
|
|
158
389
|
|
|
159
390
|
This is Ruby Maat, a Ruby port of Code Maat - a program used to collect statistics from a VCS.
|
|
160
391
|
|
|
161
|
-
Usage:
|
|
392
|
+
Usage:
|
|
393
|
+
ruby-maat -l log-file -c vcs-type [options] # Run analysis on existing log
|
|
394
|
+
ruby-maat --generate-log -c vcs-type [options] # Generate log file
|
|
395
|
+
ruby-maat --generate-log --interactive -c vcs-type # Interactive log generation
|
|
162
396
|
|
|
163
397
|
Options:
|
|
164
398
|
BANNER
|
|
@@ -170,8 +404,22 @@ module RubyMaat
|
|
|
170
404
|
|
|
171
405
|
def validate_required_options!
|
|
172
406
|
missing = []
|
|
173
|
-
|
|
174
|
-
|
|
407
|
+
|
|
408
|
+
# In interactive mode, we can detect everything
|
|
409
|
+
if @options[:interactive]
|
|
410
|
+
# Interactive mode can work with no other options
|
|
411
|
+
return
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Log file is only required when not generating logs
|
|
415
|
+
unless @options[:generate_log] || @options[:log]
|
|
416
|
+
missing << "log file (-l/--log)"
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# VCS is required for non-interactive modes
|
|
420
|
+
unless @options[:version_control]
|
|
421
|
+
missing << "version control system (-c/--version-control)"
|
|
422
|
+
end
|
|
175
423
|
|
|
176
424
|
raise ArgumentError, "Missing required options: #{missing.join(", ")}" unless missing.empty?
|
|
177
425
|
|