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.
@@ -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
- app = App.new(@options)
27
- app.run
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: ruby-maat -l log-file -c vcs-type [options]
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
- missing << "log file (-l/--log)" unless @options[:log]
174
- missing << "version control system (-c/--version-control)" unless @options[:version_control]
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