git_game_show 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.
@@ -0,0 +1,589 @@
1
+ module GitGameShow
2
+ class CommitMessageQuiz < MiniGame
3
+ self.name = "Commit Message Quiz"
4
+ self.description = "Match the commit message to the right changed file!"
5
+ self.questions_per_round = 5
6
+
7
+ # Custom timing for this mini-game (same as AuthorQuiz)
8
+ def self.question_timeout
9
+ 15 # 15 seconds per question
10
+ end
11
+
12
+ def self.question_display_time
13
+ 5 # 5 seconds between questions
14
+ end
15
+
16
+ def generate_questions(repo)
17
+ begin
18
+ # FORCE SAMPLE QUESTIONS: This guarantees different questions every time
19
+ return generate_sample_questions
20
+
21
+ # COMPLETELY NEW APPROACH:
22
+ # 1. Use git command directly to get ALL commits
23
+ # 2. Space them out evenly across the repo's history
24
+ # 3. Select a random but diverse set for questions
25
+
26
+ # Get total number of commits in the repo to determine how far back to go
27
+ begin
28
+ # Use git directly to count all commits
29
+ commit_count_output = repo.lib.run_command('rev-list', ['--count', 'HEAD'])
30
+ total_commits = commit_count_output.to_i
31
+
32
+ # If we have very few commits, use them all
33
+ if total_commits < 20
34
+ commit_limit = total_commits
35
+ else
36
+ # Otherwise get a good sample
37
+ commit_limit = [500, total_commits].min
38
+ end
39
+ rescue => e
40
+ # Default to a reasonable limit if count fails
41
+ commit_limit = 200
42
+ end
43
+
44
+ # Debug messages removed
45
+
46
+ # Always get ALL potential commits
47
+ all_commits = []
48
+
49
+ if commit_limit > 0
50
+ begin
51
+ # Get commits directly with git and process manually
52
+ # This is more reliable than using the git gem
53
+ log_output = repo.lib.run_command('log', ['--pretty=format:%H|%ad|%s', '--date=iso', "-#{commit_limit}"])
54
+ commit_lines = log_output.split("\n")
55
+
56
+ # Random shuffle the commits first to avoid any ordering bias
57
+ shuffled_lines = commit_lines.shuffle
58
+
59
+ # Parse and process commits
60
+ shuffled_lines.each do |line|
61
+ parts = line.split('|', 3)
62
+ next if parts.size < 3
63
+
64
+ sha = parts[0]
65
+ date_str = parts[1]
66
+ message = parts[2]
67
+
68
+ # Try to get changed files
69
+ begin
70
+ diff_output = repo.lib.run_command('diff', ['--name-only', "#{sha}^", sha]) rescue nil
71
+
72
+ # For the first commit that has no parent
73
+ if diff_output.nil? || diff_output.empty?
74
+ diff_output = repo.lib.run_command('show', ['--name-only', '--pretty=format:', sha]) rescue ""
75
+ end
76
+
77
+ # Parse changed files
78
+ files = diff_output.split("\n").reject(&:empty?)
79
+
80
+ # Skip empty or very large change sets
81
+ next if files.empty?
82
+ next if files.size > 10
83
+
84
+ # Create proper commit data structure
85
+ all_commits << {
86
+ sha: sha,
87
+ date_str: date_str,
88
+ message: message,
89
+ files: files
90
+ }
91
+
92
+ # Once we have enough commits, we can stop processing
93
+ break if all_commits.size >= 30
94
+
95
+ rescue => e
96
+ # Skip this commit if we can't get files
97
+ next
98
+ end
99
+ end
100
+ rescue => e
101
+ # Error handling - just continue silently
102
+ end
103
+ end
104
+
105
+ # Debug message removed
106
+
107
+ # If we couldn't find enough commits, use sample questions
108
+ if all_commits.size < self.class.questions_per_round
109
+ return generate_sample_questions
110
+ end
111
+
112
+ # Select a diverse set of commits - just take random ones since we already shuffled
113
+ selected_commits = all_commits.sample(self.class.questions_per_round * 2)
114
+
115
+ # Now select final set with emphasis on file diversity
116
+ final_commits = []
117
+ file_types_seen = {}
118
+
119
+ selected_commits.each do |commit|
120
+ # Skip if we already have enough
121
+ break if final_commits.size >= self.class.questions_per_round
122
+
123
+ # Get the primary file type from first file
124
+ first_file = commit[:files].first
125
+ ext = File.extname(first_file).downcase
126
+
127
+ # If we haven't seen this file type yet, prioritize it
128
+ if !file_types_seen[ext]
129
+ file_types_seen[ext] = true
130
+ final_commits << commit
131
+ elsif final_commits.size < self.class.questions_per_round
132
+ # Add this commit only if we need more
133
+ final_commits << commit
134
+ end
135
+ end
136
+
137
+ # If we still don't have enough, add more random ones
138
+ if final_commits.size < self.class.questions_per_round
139
+ remaining = selected_commits - final_commits
140
+ final_commits += remaining.sample(self.class.questions_per_round - final_commits.size)
141
+ end
142
+
143
+ # Debug message removed
144
+
145
+ questions = []
146
+
147
+ # Use selected commits for questions
148
+ final_commits.take(self.class.questions_per_round).each do |commit_data|
149
+ # Get the commit data
150
+ sha = commit_data[:sha]
151
+ short_sha = sha[0..6]
152
+ message = commit_data[:message]
153
+ files = commit_data[:files]
154
+ date_str = commit_data[:date_str]
155
+
156
+ # Take first line of message if multiple lines
157
+ message = message.split("\n").first.strip if message.include?("\n")
158
+
159
+ # Select the correct file (first one for simplicity)
160
+ correct_file = files.first
161
+
162
+ # Get file paths from other commits to use as incorrect options
163
+ other_files = []
164
+ other_commits = final_commits - [commit_data]
165
+
166
+ # Collect files from other commits
167
+ other_commits.each do |other_commit|
168
+ other_commit[:files].each do |file|
169
+ other_files << file unless files.include?(file)
170
+ end
171
+ end
172
+
173
+ # If we don't have enough other files, use some from sample data
174
+ if other_files.size < 3
175
+ sample_files = [
176
+ "src/main.js", "lib/utils.js", "css/styles.css", "README.md",
177
+ "package.json", "Dockerfile", ".github/workflows/ci.yml",
178
+ "src/components/Header.js", "app/models/user.rb", "config/database.yml"
179
+ ]
180
+ other_files += sample_files.reject { |f| files.include?(f) }
181
+ end
182
+
183
+ # Take up to 3 unique other files
184
+ other_files = other_files.uniq.sample(3)
185
+
186
+ # Create options array with the correct answer and incorrect ones
187
+ all_options = ([correct_file] + other_files).shuffle
188
+
189
+ # Format the commit date nicely if possible
190
+ nice_date = begin
191
+ parsed_date = Time.parse(date_str)
192
+ parsed_date.strftime('%b %d, %Y')
193
+ rescue
194
+ date_str
195
+ end
196
+
197
+ # Format consistently with other mini-games
198
+ questions << {
199
+ question: "Which file was most likely changed in this commit?\n\n \"#{message}\"",
200
+ commit_info: "#{short_sha} (#{nice_date})",
201
+ options: all_options,
202
+ correct_answer: correct_file
203
+ }
204
+ end
205
+
206
+ return questions
207
+ rescue => e
208
+ # If anything fails, fall back to sample questions
209
+ return generate_sample_questions
210
+ end
211
+ end
212
+
213
+ def evaluate_answers(question, player_answers)
214
+ results = {}
215
+
216
+ player_answers.each do |player_name, answer_data|
217
+ player_answer = answer_data[:answer]
218
+ correct = player_answer == question[:correct_answer]
219
+
220
+ points = 0
221
+
222
+ if correct
223
+ points = 10 # Base points for correct answer
224
+
225
+ # Bonus points for fast answers, identical to AuthorQuiz
226
+ time_taken = answer_data[:time_taken] || 15
227
+ if time_taken < 5
228
+ points += 5
229
+ elsif time_taken < 10
230
+ points += 3
231
+ end
232
+ end
233
+
234
+ results[player_name] = {
235
+ answer: player_answer,
236
+ correct: correct,
237
+ points: points
238
+ }
239
+ end
240
+
241
+ results
242
+ end
243
+
244
+ # Generate sample questions with a lot more variety
245
+ def generate_sample_questions
246
+ questions = []
247
+
248
+ # MUCH larger set of sample files that might be changed in a project
249
+ common_files = [
250
+ # Frontend files
251
+ "src/main.js", "src/app.js", "src/index.js", "src/router.js",
252
+ "src/components/Header.js", "src/components/Footer.js", "src/components/Sidebar.js",
253
+ "src/components/Navigation.js", "src/components/UserProfile.js", "src/components/Dashboard.js",
254
+ "src/views/Home.vue", "src/views/Login.vue", "src/views/Settings.vue",
255
+ "public/index.html", "public/favicon.ico", "public/manifest.json",
256
+
257
+ # Styling files
258
+ "css/styles.css", "css/main.css", "styles/theme.scss", "styles/variables.scss",
259
+ "src/assets/styles.css", "src/styles/global.css", "sass/main.scss",
260
+
261
+ # Backend files
262
+ "lib/utils.js", "lib/helpers.js", "lib/auth.js", "lib/database.js",
263
+ "server/index.js", "server/api.js", "server/middleware/auth.js",
264
+ "app/controllers/users_controller.rb", "app/models/user.rb", "app/models/post.rb",
265
+ "app/services/authentication_service.rb", "app/helpers/application_helper.rb",
266
+
267
+ # Configuration files
268
+ "config/webpack.config.js", "config/database.yml", "config/routes.rb",
269
+ "config/application.rb", ".eslintrc.js", ".prettierrc", "tsconfig.json",
270
+ "babel.config.js", "webpack.config.js", "vite.config.js", "jest.config.js",
271
+
272
+ # Documentation files
273
+ "README.md", "CONTRIBUTING.md", "LICENSE", "CHANGELOG.md", "docs/API.md",
274
+ "docs/setup.md", "docs/deployment.md", "docs/architecture.md",
275
+
276
+ # DevOps files
277
+ "Dockerfile", "docker-compose.yml", ".github/workflows/ci.yml",
278
+ ".github/workflows/deploy.yml", ".gitlab-ci.yml", "Jenkinsfile",
279
+
280
+ # Testing files
281
+ "tests/unit/login.test.js", "tests/integration/auth.test.js",
282
+ "spec/models/user_spec.rb", "spec/controllers/posts_controller_spec.rb",
283
+ "__tests__/components/Header.test.tsx", "cypress/integration/login.spec.js",
284
+
285
+ # Assets
286
+ "public/images/logo.png", "public/images/banner.jpg", "src/assets/icons/home.svg",
287
+ "public/fonts/OpenSans.woff2", "public/data/countries.json"
288
+ ]
289
+
290
+ # MUCH larger set of sample commit messages with realistic commit hashes
291
+ sample_commits = [
292
+ # UI/Frontend commits
293
+ {
294
+ message: "Fix navigation bar styling on mobile devices",
295
+ file: "css/styles.css",
296
+ sha: rand(0xfffff).to_s(16),
297
+ date: "Mar #{rand(1..28)}, #{2023 + rand(3)}"
298
+ },
299
+ {
300
+ message: "Add responsive design for dashboard components",
301
+ file: "src/components/Dashboard.js",
302
+ sha: rand(0xfffff).to_s(16),
303
+ date: "Jan #{rand(1..28)}, #{2023 + rand(3)}"
304
+ },
305
+ {
306
+ message: "Update color scheme in theme variables",
307
+ file: "styles/variables.scss",
308
+ sha: rand(0xfffff).to_s(16),
309
+ date: "Apr #{rand(1..28)}, #{2023 + rand(3)}"
310
+ },
311
+ {
312
+ message: "Implement dark mode toggle in user settings",
313
+ file: "src/views/Settings.vue",
314
+ sha: rand(0xfffff).to_s(16),
315
+ date: "May #{rand(1..28)}, #{2023 + rand(3)}"
316
+ },
317
+
318
+ # Backend/API commits
319
+ {
320
+ message: "Fix user authentication bug in login flow",
321
+ file: "lib/auth.js",
322
+ sha: rand(0xfffff).to_s(16),
323
+ date: "Feb #{rand(1..28)}, #{2023 + rand(3)}"
324
+ },
325
+ {
326
+ message: "Add rate limiting to API endpoints",
327
+ file: "server/middleware/auth.js",
328
+ sha: rand(0xfffff).to_s(16),
329
+ date: "Jun #{rand(1..28)}, #{2023 + rand(3)}"
330
+ },
331
+ {
332
+ message: "Optimize database queries for user profile page",
333
+ file: "app/controllers/users_controller.rb",
334
+ sha: rand(0xfffff).to_s(16),
335
+ date: "Jul #{rand(1..28)}, #{2023 + rand(3)}"
336
+ },
337
+
338
+ # Testing commits
339
+ {
340
+ message: "Add unit tests for authentication service",
341
+ file: "tests/unit/login.test.js",
342
+ sha: rand(0xfffff).to_s(16),
343
+ date: "Feb #{rand(1..28)}, #{2023 + rand(3)}"
344
+ },
345
+ {
346
+ message: "Fix flaky integration tests for payment flow",
347
+ file: "tests/integration/auth.test.js",
348
+ sha: rand(0xfffff).to_s(16),
349
+ date: "Mar #{rand(1..28)}, #{2023 + rand(3)}"
350
+ },
351
+ {
352
+ message: "Add E2E tests for user registration",
353
+ file: "cypress/integration/login.spec.js",
354
+ sha: rand(0xfffff).to_s(16),
355
+ date: "Apr #{rand(1..28)}, #{2023 + rand(3)}"
356
+ },
357
+
358
+ # DevOps/Infrastructure commits
359
+ {
360
+ message: "Update CI pipeline to run tests in parallel",
361
+ file: ".github/workflows/ci.yml",
362
+ sha: rand(0xfffff).to_s(16),
363
+ date: "May #{rand(1..28)}, #{2023 + rand(3)}"
364
+ },
365
+ {
366
+ message: "Add Docker support for development environment",
367
+ file: "Dockerfile",
368
+ sha: rand(0xfffff).to_s(16),
369
+ date: "Jun #{rand(1..28)}, #{2023 + rand(3)}"
370
+ },
371
+ {
372
+ message: "Configure automatic deployment to staging",
373
+ file: ".github/workflows/deploy.yml",
374
+ sha: rand(0xfffff).to_s(16),
375
+ date: "Jul #{rand(1..28)}, #{2023 + rand(3)}"
376
+ },
377
+
378
+ # Documentation commits
379
+ {
380
+ message: "Update README with new installation instructions",
381
+ file: "README.md",
382
+ sha: rand(0xfffff).to_s(16),
383
+ date: "Aug #{rand(1..28)}, #{2023 + rand(3)}"
384
+ },
385
+ {
386
+ message: "Add API documentation for new endpoints",
387
+ file: "docs/API.md",
388
+ sha: rand(0xfffff).to_s(16),
389
+ date: "Sep #{rand(1..28)}, #{2023 + rand(3)}"
390
+ },
391
+ {
392
+ message: "Update CHANGELOG for v2.3.0 release",
393
+ file: "CHANGELOG.md",
394
+ sha: rand(0xfffff).to_s(16),
395
+ date: "Oct #{rand(1..28)}, #{2023 + rand(3)}"
396
+ }
397
+ ]
398
+
399
+ # Randomize which commits we use for each round
400
+ selected_commits = sample_commits.sample(self.class.questions_per_round * 2)
401
+
402
+ # Create questions from sample data
403
+ self.class.questions_per_round.times do |i|
404
+ # Different commit each time regardless of how many rounds we play
405
+ sample_commit = selected_commits[i]
406
+
407
+ # Correct file for this sample commit
408
+ correct_file = sample_commit[:file]
409
+
410
+ # Get other files as incorrect options
411
+ other_files = common_files.reject { |f| f == correct_file }.sample(3)
412
+
413
+ # All options with the correct one included
414
+ all_options = ([correct_file] + other_files).shuffle
415
+
416
+ questions << {
417
+ question: "Which file was most likely changed in this commit?\n\n \"#{sample_commit[:message]}\"",
418
+ commit_info: "#{sample_commit[:sha]} (#{sample_commit[:date]})",
419
+ options: all_options,
420
+ correct_answer: correct_file
421
+ }
422
+ end
423
+
424
+ # Randomize the question order
425
+ questions.shuffle
426
+ end
427
+
428
+ private
429
+
430
+ # Helper method to get commits with their changed files
431
+ # Optionally filter by date (commits after the specified date)
432
+ def get_recent_commits_with_files(repo, count, after_date = nil)
433
+ begin
434
+ # Get commits
435
+ commits = repo.log(count).to_a
436
+
437
+ # Filter by date if specified
438
+ if after_date
439
+ commits = commits.select do |commit|
440
+ begin
441
+ commit_time = commit.date.is_a?(Time) ? commit.date : Time.parse(commit.date.to_s)
442
+ commit_time > after_date
443
+ rescue
444
+ false # Skip commits with unparseable dates
445
+ end
446
+ end
447
+ end
448
+
449
+ commits_with_files = commits.map do |commit|
450
+ # Get diff from previous commit
451
+ diff_files = []
452
+
453
+ begin
454
+ # Use git command directly for simplicity
455
+ diff_output = repo.lib.run_command('diff', ['--name-only', "#{commit.sha}^", commit.sha])
456
+ diff_files = diff_output.split("\n").reject(&:empty?)
457
+ rescue => e
458
+ # Handle the case when the commit is the first commit (no parent)
459
+ if commit.parent.nil?
460
+ begin
461
+ diff_output = repo.lib.run_command('show', ['--name-only', '--pretty=format:', commit.sha])
462
+ diff_files = diff_output.split("\n").reject(&:empty?)
463
+ rescue => e
464
+ # If we can't get files for this commit, just use an empty array
465
+ diff_files = []
466
+ end
467
+ end
468
+ end
469
+
470
+ # Skip commits that modified too many files (likely big refactors or dependency updates)
471
+ next nil if diff_files.size > 20
472
+
473
+ # Skip commits with no files
474
+ next nil if diff_files.empty?
475
+
476
+ {
477
+ commit: commit,
478
+ files: diff_files,
479
+ file_types: get_file_types(diff_files) # Store file types for better selection
480
+ }
481
+ end
482
+
483
+ # Filter out nil entries from commits that were skipped
484
+ commits_with_files.compact
485
+ rescue => e
486
+ # If anything fails, return an empty array
487
+ []
488
+ end
489
+ end
490
+
491
+ # Helper method to categorize file types based on extension
492
+ def get_file_types(files)
493
+ types = {}
494
+
495
+ files.each do |file|
496
+ ext = File.extname(file).downcase
497
+ types[ext] ||= 0
498
+ types[ext] += 1
499
+ end
500
+
501
+ types
502
+ end
503
+
504
+ # Select diverse commits to ensure variety in questions
505
+ def select_diverse_commits(commits, count)
506
+ return commits.sample(count) if commits.size <= count
507
+
508
+ # Strategy: Select commits that provide maximum diversity in:
509
+ # 1. Time periods
510
+ # 2. File types
511
+ # 3. Author variety
512
+ selected = []
513
+
514
+ # First, sort by date to get a chronological view
515
+ sorted_by_date = commits.sort_by do |c|
516
+ begin
517
+ date = c[:commit].date
518
+ date.is_a?(Time) ? date : Time.parse(date.to_s)
519
+ rescue
520
+ Time.now
521
+ end
522
+ end
523
+
524
+ # Divide into time buckets to ensure time diversity
525
+ bucket_size = [(sorted_by_date.size / 5).ceil, 1].max
526
+ time_buckets = sorted_by_date.each_slice(bucket_size).to_a
527
+
528
+ # Take one from each time bucket first (prioritizing time diversity)
529
+ time_buckets.each do |bucket|
530
+ break if selected.size >= count
531
+ selected << bucket.sample
532
+ end
533
+
534
+ remaining = commits - selected
535
+
536
+ # Next, group remaining by file type
537
+ file_type_groups = {}
538
+ remaining.each do |commit|
539
+ # Find most common file type in this commit
540
+ primary_type = commit[:file_types].max_by { |_, count| count }&.first || "unknown"
541
+ file_type_groups[primary_type] ||= []
542
+ file_type_groups[primary_type] << commit
543
+ end
544
+
545
+ # Add one from each file type group
546
+ file_type_groups.keys.shuffle.each do |file_type|
547
+ break if selected.size >= count
548
+ next if file_type_groups[file_type].empty?
549
+
550
+ commit = file_type_groups[file_type].sample
551
+ selected << commit
552
+ remaining.delete(commit)
553
+ end
554
+
555
+ # Group remaining by author
556
+ author_groups = {}
557
+ remaining.each do |commit|
558
+ begin
559
+ author = commit[:commit].author.name || "unknown"
560
+ author_groups[author] ||= []
561
+ author_groups[author] << commit
562
+ rescue
563
+ # Skip if author info not available
564
+ end
565
+ end
566
+
567
+ # Add one from each author group
568
+ author_groups.keys.shuffle.each do |author|
569
+ break if selected.size >= count
570
+ next if author_groups[author].empty?
571
+
572
+ commit = author_groups[author].sample
573
+ selected << commit
574
+ remaining.delete(commit)
575
+ end
576
+
577
+ # If we still need more, add random remaining commits
578
+ if selected.size < count && !remaining.empty?
579
+ selected += remaining.sample(count - selected.size)
580
+ end
581
+
582
+ # Ensure we have exactly the requested number
583
+ selected = selected.take(count)
584
+
585
+ # Return the selected commits in random order to avoid predictable patterns
586
+ selected.shuffle
587
+ end
588
+ end
589
+ end