ruby-maat 1.0.0 → 1.3.4
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 +178 -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/app.rb +25 -0
- data/lib/ruby_maat/change_record.rb +13 -3
- data/lib/ruby_maat/cli.rb +259 -5
- data/lib/ruby_maat/generators/base_generator.rb +289 -0
- data/lib/ruby_maat/generators/git_generator.rb +207 -0
- data/lib/ruby_maat/generators/svn_generator.rb +201 -0
- data/lib/ruby_maat/groupers/merge_commit_grouper.rb +152 -0
- data/lib/ruby_maat/parsers/git2_parser.rb +33 -10
- 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/lib/ruby_maat.rb +1 -0
- data/release-please-config.json +57 -0
- metadata +19 -11
- data/.mailmap +0 -3
- data/.release-please-config.json +0 -33
- data/CI_CD_SETUP.md +0 -180
- data/README_RUBY.md +0 -300
- data/RELEASE_PLEASE_SETUP.md +0 -198
- data/RUBY_MAAT.md +0 -227
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|
|
|
@@ -131,6 +362,12 @@ module RubyMaat
|
|
|
131
362
|
raise ArgumentError, "Invalid date format for --age-time-now: #{date_str}. Use YYYY-MM-dd format."
|
|
132
363
|
end
|
|
133
364
|
|
|
365
|
+
opts.on("--group-by-merge",
|
|
366
|
+
"Group commits by merge commit for PR-level coupling analysis. " \
|
|
367
|
+
"Requires log generated with parent hashes (use --preset pr-coupling).") do
|
|
368
|
+
@options[:group_by_merge] = true
|
|
369
|
+
end
|
|
370
|
+
|
|
134
371
|
opts.on("--verbose-results",
|
|
135
372
|
"Includes additional analysis details together with the results. Only implemented for change coupling.") do
|
|
136
373
|
@options[:verbose_results] = true
|
|
@@ -158,7 +395,10 @@ module RubyMaat
|
|
|
158
395
|
|
|
159
396
|
This is Ruby Maat, a Ruby port of Code Maat - a program used to collect statistics from a VCS.
|
|
160
397
|
|
|
161
|
-
Usage:
|
|
398
|
+
Usage:
|
|
399
|
+
ruby-maat -l log-file -c vcs-type [options] # Run analysis on existing log
|
|
400
|
+
ruby-maat --generate-log -c vcs-type [options] # Generate log file
|
|
401
|
+
ruby-maat --generate-log --interactive -c vcs-type # Interactive log generation
|
|
162
402
|
|
|
163
403
|
Options:
|
|
164
404
|
BANNER
|
|
@@ -170,8 +410,22 @@ module RubyMaat
|
|
|
170
410
|
|
|
171
411
|
def validate_required_options!
|
|
172
412
|
missing = []
|
|
173
|
-
|
|
174
|
-
|
|
413
|
+
|
|
414
|
+
# In interactive mode, we can detect everything
|
|
415
|
+
if @options[:interactive]
|
|
416
|
+
# Interactive mode can work with no other options
|
|
417
|
+
return
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Log file is only required when not generating logs
|
|
421
|
+
unless @options[:generate_log] || @options[:log]
|
|
422
|
+
missing << "log file (-l/--log)"
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# VCS is required for non-interactive modes
|
|
426
|
+
unless @options[:version_control]
|
|
427
|
+
missing << "version control system (-c/--version-control)"
|
|
428
|
+
end
|
|
175
429
|
|
|
176
430
|
raise ArgumentError, "Missing required options: #{missing.join(", ")}" unless missing.empty?
|
|
177
431
|
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
module RubyMaat
|
|
7
|
+
module Generators
|
|
8
|
+
class BaseGenerator
|
|
9
|
+
attr_reader :repository_path, :options
|
|
10
|
+
|
|
11
|
+
def initialize(repository_path = ".", options = {})
|
|
12
|
+
@repository_path = File.expand_path(repository_path)
|
|
13
|
+
@options = options
|
|
14
|
+
validate_repository!
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def generate_log(output_file = nil, **generation_options)
|
|
18
|
+
merged_options = @options.merge(generation_options)
|
|
19
|
+
|
|
20
|
+
if output_file
|
|
21
|
+
generate_persistent_log(output_file, merged_options)
|
|
22
|
+
else
|
|
23
|
+
generate_temporary_log(merged_options)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def available_presets
|
|
28
|
+
[]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def interactive_generate
|
|
32
|
+
unless $stdin.tty?
|
|
33
|
+
raise "Interactive mode requires a terminal (TTY). Use --generate-log with presets instead."
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
puts "Interactive log generation for #{vcs_name}"
|
|
37
|
+
puts "Repository: #{@repository_path}"
|
|
38
|
+
puts
|
|
39
|
+
|
|
40
|
+
preset = choose_preset
|
|
41
|
+
options = gather_custom_options(preset)
|
|
42
|
+
|
|
43
|
+
save_log = ask_yes_no("Save log to file for future use?")
|
|
44
|
+
|
|
45
|
+
if save_log
|
|
46
|
+
default_filename = default_log_filename
|
|
47
|
+
filename = ask_string("Log filename", default_filename)
|
|
48
|
+
generate_log(filename, **options)
|
|
49
|
+
else
|
|
50
|
+
generate_log(nil, **options)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def interactive_generate_for_analysis(analysis_name, analysis_options = {})
|
|
55
|
+
unless $stdin.tty?
|
|
56
|
+
raise "Interactive mode requires a terminal (TTY). Use --generate-log with presets instead."
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
puts "Log generation for #{vcs_name}"
|
|
60
|
+
puts "Repository: #{@repository_path}"
|
|
61
|
+
puts "Analysis: #{RubyMaat::AnalysisPresets.analysis_description(analysis_name)}"
|
|
62
|
+
puts
|
|
63
|
+
|
|
64
|
+
# Get analysis-specific presets
|
|
65
|
+
presets = RubyMaat::AnalysisPresets.presets_for_analysis(analysis_name)
|
|
66
|
+
|
|
67
|
+
if presets.empty?
|
|
68
|
+
puts "No presets available for #{analysis_name}"
|
|
69
|
+
options = gather_custom_options({})
|
|
70
|
+
else
|
|
71
|
+
preset_options = choose_analysis_preset(analysis_name, presets)
|
|
72
|
+
options = gather_custom_options(preset_options)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Merge with any analysis-specific options
|
|
76
|
+
options.merge!(analysis_options)
|
|
77
|
+
|
|
78
|
+
save_log = ask_yes_no("Save log to file for future use?", false)
|
|
79
|
+
|
|
80
|
+
if save_log
|
|
81
|
+
default_filename = "#{analysis_name}_#{default_log_filename}"
|
|
82
|
+
filename = ask_string("Log filename", default_filename)
|
|
83
|
+
generate_log(filename, **options)
|
|
84
|
+
else
|
|
85
|
+
generate_log(nil, **options)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
protected
|
|
90
|
+
|
|
91
|
+
def vcs_name
|
|
92
|
+
self.class.name.split("::").last.sub("Generator", "").downcase
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def validate_repository!
|
|
96
|
+
raise ArgumentError, "Repository path does not exist: #{@repository_path}" unless Dir.exist?(@repository_path)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def build_command(options)
|
|
100
|
+
raise NotImplementedError, "Subclasses must implement build_command"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def execute_command(command)
|
|
104
|
+
# Show command preview and ask for confirmation
|
|
105
|
+
show_command_preview(command)
|
|
106
|
+
|
|
107
|
+
puts "Executing: #{command}" if @options[:verbose]
|
|
108
|
+
|
|
109
|
+
# Use proper subprocess execution to prevent command injection
|
|
110
|
+
require "open3"
|
|
111
|
+
|
|
112
|
+
# Change to repository directory and execute command safely
|
|
113
|
+
result, error, status = Open3.capture3(command, chdir: @repository_path)
|
|
114
|
+
if status.exitstatus != 0
|
|
115
|
+
combined_output = result + error
|
|
116
|
+
raise "Command failed with exit code #{status.exitstatus}: #{combined_output}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
result
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def default_log_filename
|
|
123
|
+
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
|
|
124
|
+
"#{vcs_name}_log_#{timestamp}.log"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def generate_persistent_log(output_file, options)
|
|
130
|
+
command = build_command(options)
|
|
131
|
+
output = execute_command(command)
|
|
132
|
+
|
|
133
|
+
File.write(output_file, output)
|
|
134
|
+
puts "Log generated: #{output_file}" unless @options[:quiet]
|
|
135
|
+
|
|
136
|
+
output_file
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def generate_temporary_log(options)
|
|
140
|
+
command = build_command(options)
|
|
141
|
+
execute_command(command)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def choose_preset
|
|
145
|
+
presets = available_presets
|
|
146
|
+
|
|
147
|
+
if presets.empty?
|
|
148
|
+
puts "No presets available for #{vcs_name}"
|
|
149
|
+
return {}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
puts "Available presets:"
|
|
153
|
+
presets.each_with_index do |(name, description), index|
|
|
154
|
+
puts " #{index + 1}. #{name} - #{description[:description]}"
|
|
155
|
+
end
|
|
156
|
+
puts " #{presets.length + 1}. Custom - Enter custom options"
|
|
157
|
+
|
|
158
|
+
choice = ask_integer("Choose preset", 1, presets.length + 1)
|
|
159
|
+
|
|
160
|
+
if choice <= presets.length
|
|
161
|
+
preset_name = presets.keys[choice - 1]
|
|
162
|
+
presets[preset_name][:options]
|
|
163
|
+
else
|
|
164
|
+
{}
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def choose_analysis_preset(analysis_name, presets)
|
|
169
|
+
puts "Available presets for #{analysis_name}:"
|
|
170
|
+
presets.each_with_index do |(name, config), index|
|
|
171
|
+
puts " #{index + 1}. #{name} - #{config[:description]}"
|
|
172
|
+
end
|
|
173
|
+
puts " #{presets.length + 1}. Custom - Enter custom options"
|
|
174
|
+
|
|
175
|
+
choice = ask_integer("Choose preset", 1, presets.length + 1)
|
|
176
|
+
|
|
177
|
+
if choice <= presets.length
|
|
178
|
+
preset_name = presets.keys[choice - 1]
|
|
179
|
+
presets[preset_name]
|
|
180
|
+
else
|
|
181
|
+
{}
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def gather_custom_options(base_options)
|
|
186
|
+
options = base_options.dup
|
|
187
|
+
|
|
188
|
+
puts "\nCustom options (press Enter to keep default):"
|
|
189
|
+
|
|
190
|
+
if supports_date_filtering?
|
|
191
|
+
since_date = ask_string("Since date (YYYY-MM-DD)", options[:since])
|
|
192
|
+
options[:since] = since_date unless since_date.empty?
|
|
193
|
+
|
|
194
|
+
until_date = ask_string("Until date (YYYY-MM-DD)", options[:until])
|
|
195
|
+
options[:until] = until_date unless until_date.empty?
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
gather_vcs_specific_options(options)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def gather_vcs_specific_options(options)
|
|
202
|
+
options
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def supports_date_filtering?
|
|
206
|
+
true
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def ask_string(prompt, default = nil)
|
|
210
|
+
default_text = default ? " [#{default}]" : ""
|
|
211
|
+
print "#{prompt}#{default_text}: "
|
|
212
|
+
response = $stdin.gets
|
|
213
|
+
return default || "" if response.nil?
|
|
214
|
+
response = response.chomp
|
|
215
|
+
response.empty? ? (default || "") : response
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def ask_integer(prompt, min = nil, max = nil)
|
|
219
|
+
attempts = 0
|
|
220
|
+
max_attempts = 10
|
|
221
|
+
|
|
222
|
+
loop do
|
|
223
|
+
attempts += 1
|
|
224
|
+
if attempts > max_attempts
|
|
225
|
+
raise "Too many invalid attempts. Exiting interactive mode."
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
response = ask_string(prompt)
|
|
229
|
+
|
|
230
|
+
# Handle empty response or non-interactive mode
|
|
231
|
+
if response.nil? || response.empty?
|
|
232
|
+
puts "Please enter a valid number"
|
|
233
|
+
next
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
begin
|
|
237
|
+
value = Integer(response)
|
|
238
|
+
return value if (min.nil? || value >= min) && (max.nil? || value <= max)
|
|
239
|
+
puts "Please enter a number between #{min} and #{max}"
|
|
240
|
+
rescue ArgumentError
|
|
241
|
+
puts "Please enter a valid number"
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def ask_yes_no(prompt, default = nil)
|
|
247
|
+
default_text = case default
|
|
248
|
+
when true then " [Y/n]"
|
|
249
|
+
when false then " [y/N]"
|
|
250
|
+
else " [y/n]"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
loop do
|
|
254
|
+
response = ask_string("#{prompt}#{default_text}").downcase
|
|
255
|
+
|
|
256
|
+
case response
|
|
257
|
+
when "y", "yes"
|
|
258
|
+
return true
|
|
259
|
+
when "n", "no"
|
|
260
|
+
return false
|
|
261
|
+
when ""
|
|
262
|
+
return default unless default.nil?
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
puts "Please enter 'y' or 'n'"
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def show_command_preview(command)
|
|
270
|
+
# Skip preview in non-interactive modes, when quiet is set, or during tests
|
|
271
|
+
return if @options[:quiet] || !$stdin.tty? || test_environment?
|
|
272
|
+
|
|
273
|
+
puts "\n" + "=" * 60
|
|
274
|
+
puts "COMMAND PREVIEW"
|
|
275
|
+
puts "=" * 60
|
|
276
|
+
puts "Repository: #{@repository_path}"
|
|
277
|
+
puts "Command: #{command}"
|
|
278
|
+
puts "=" * 60
|
|
279
|
+
puts "\nPress Enter to execute this command..."
|
|
280
|
+
$stdin.gets
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def test_environment?
|
|
284
|
+
# Check if we're in a test environment
|
|
285
|
+
!!(defined?(RSpec) || ENV["RUBY_MAAT_TEST"] == "true" || ENV["RSPEC_CORE_PID"])
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|