git_game_show 0.1.7 → 0.1.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0eaec4e8df244f6bce785020f5e51e64c3ce2a4a4a0f34e24b35067d00ff46b
4
- data.tar.gz: 1370dec1ae250bce28fc021353353a1a3fcf31b19e376128065377a5e7d0b280
3
+ metadata.gz: 719118cfd107443e62e4c5e91fe2ee425bf58d30b9c681df744e9e8fbc25f81d
4
+ data.tar.gz: 80cb0a7b0f9653d33e0e58057eb9fc85df59757486e452c22de2ea3508a0bcb2
5
5
  SHA512:
6
- metadata.gz: 9b32957cf357b2cd3d1d04c48fc285307cc948e7fcb150ec364a1e7ec2d41a552e894207935b8912729ffa6d763a42006508b741afe65af8dab0110227cd3c6e
7
- data.tar.gz: 1993c667e5c49e90c4c36a7f2888e61573bfda38113be4fa41dd91d095a339023bfd242d5fcbf87c07fae4f3da563815862d103e7b5cfce3665acccf85c15e13
6
+ metadata.gz: 6ba0dec0a4e51517994a4c574cb1d1c738780e2a812a4d516fb53f18d46d9c08661ec924a67dbb228d4efffe360e0f47bebe804464fe7b83e10ad053a859da96
7
+ data.tar.gz: acc62cb0dcaf6ae4d6249b8a1b4b5b6f03fc3d2d19016dc2f65a64991d31069b7b5b1786ff5c1566575c193473ba36d4ac3285385f46d74f81f30e6e21449a10
@@ -1592,7 +1592,7 @@ module GitGameShow
1592
1592
  # Enable all mini-games
1593
1593
  [
1594
1594
  GitGameShow::AuthorQuiz,
1595
- GitGameShow::CommitMessageQuiz,
1595
+ GitGameShow::FileQuiz,
1596
1596
  GitGameShow::CommitMessageCompletion,
1597
1597
  GitGameShow::DateOrderingQuiz
1598
1598
  ]
@@ -1,4 +1,4 @@
1
1
  module GitGameShow
2
2
  # Current version of Git Game Show
3
- VERSION = "0.1.7"
3
+ VERSION = "0.1.8"
4
4
  end
@@ -0,0 +1,278 @@
1
+ module GitGameShow
2
+ class FileQuiz < MiniGame
3
+ self.name = "File 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
+ # Get commits from the repo - based on similar approach as AuthorQuiz
19
+ # Start by getting a larger number of commits to ensure enough variety
20
+ commit_count = [self.class.questions_per_round * 10, 100].min
21
+
22
+ # Get all commits
23
+ commits = repo.log(commit_count).to_a
24
+
25
+ # Shuffle commits for better variety
26
+ commits.shuffle!
27
+
28
+ # Process commits to find ones with good file changes
29
+ valid_commits = []
30
+
31
+ commits.each do |commit|
32
+ # Get changed files for this commit
33
+ changed_files = []
34
+
35
+ begin
36
+ # Try to get the diff with previous commit
37
+ if commit.parents.empty?
38
+ # First commit - get the files directly
39
+ diff_output = repo.lib.run_command('show', ['--name-only', '--pretty=format:', commit.sha])
40
+ changed_files = diff_output.split("\n").reject(&:empty?)
41
+ else
42
+ # Regular commit with parent
43
+ diff_output = repo.lib.run_command('diff', ['--name-only', "#{commit.sha}^", commit.sha])
44
+ changed_files = diff_output.split("\n").reject(&:empty?)
45
+ end
46
+
47
+ # Skip if no files or too many files (probably a merge or refactoring)
48
+ next if changed_files.empty?
49
+ next if changed_files.size > 8 # Skip large commits that likely changed multiple unrelated files
50
+
51
+ # Create structure with relevant commit data
52
+ valid_commits << {
53
+ sha: commit.sha,
54
+ message: commit.message,
55
+ author: commit.author.name,
56
+ date: commit.date,
57
+ files: changed_files
58
+ }
59
+
60
+ # Once we have enough commits, we can stop
61
+ break if valid_commits.size >= self.class.questions_per_round * 3
62
+ rescue => e
63
+ # Skip problematic commits
64
+ next
65
+ end
66
+ end
67
+
68
+ # If we couldn't find enough good commits, fall back to samples
69
+ if valid_commits.size < self.class.questions_per_round
70
+ return generate_sample_questions
71
+ end
72
+
73
+ # Prioritize commits that modified interesting files (not just .gitignore etc.)
74
+ prioritized_commits = valid_commits.sort_by do |commit|
75
+ # Higher score = more interesting commit
76
+ score = 0
77
+
78
+ # Prioritize based on file types
79
+ commit[:files].each do |file|
80
+ ext = File.extname(file).downcase
81
+
82
+ case ext
83
+ when '.rb', '.js', '.py', '.java', '.tsx', '.jsx'
84
+ score += 3 # Source code is most interesting
85
+ when '.html', '.css', '.scss'
86
+ score += 2 # Templates and styles are interesting
87
+ when '.md', '.txt', '.json', '.yaml', '.yml'
88
+ score += 1 # Config and docs are moderately interesting
89
+ when '', '.gitignore', '.gitattributes'
90
+ score -= 1 # Less interesting files
91
+ end
92
+ end
93
+
94
+ # Prioritize based on commit message length - longer messages are often more descriptive
95
+ message_length = commit[:message].to_s.strip.length
96
+ score += [message_length / 20, 5].min
97
+
98
+ # Return negative score so highest scores come first in sort
99
+ -score
100
+ end
101
+
102
+ # Select top commits for questions
103
+ selected_commits = prioritized_commits.take(self.class.questions_per_round)
104
+
105
+ # Create questions from selected commits
106
+ questions = []
107
+
108
+ selected_commits.each do |commit|
109
+ # Choose the most interesting file as the correct answer
110
+ # (Sort by extension priority, then by path length to favor shorter paths)
111
+ files = commit[:files]
112
+
113
+ # Score files by interestingness
114
+ scored_files = files.map do |file|
115
+ ext = File.extname(file).downcase
116
+
117
+ # Start with base score by extension
118
+ score = case ext
119
+ when '.rb', '.js', '.py', '.java', '.tsx', '.jsx'
120
+ 10 # Source code is most interesting
121
+ when '.html', '.css', '.scss'
122
+ 8 # Templates and styles are interesting
123
+ when '.md', '.txt'
124
+ 6 # Documentation
125
+ when '.json', '.yaml', '.yml'
126
+ 4 # Config files
127
+ when '', '.gitignore', '.gitattributes'
128
+ 0 # Less interesting files
129
+ else
130
+ 5 # Other files are neutral
131
+ end
132
+
133
+ # Shorter paths are usually more recognizable
134
+ score -= [file.length / 10, 5].min
135
+
136
+ # Prefer files in main directories (src, lib, app) over deeply nested ones
137
+ if file.start_with?('src/', 'lib/', 'app/')
138
+ score += 3
139
+ end
140
+
141
+ [file, score]
142
+ end
143
+
144
+ # Sort by score (highest first) and select most interesting file
145
+ correct_file = scored_files.sort_by { |_, score| -score }.first[0]
146
+
147
+ # Get incorrect options from other commits
148
+ other_files = []
149
+ other_commits = selected_commits - [commit]
150
+
151
+ # Collect files from other commits
152
+ other_commits.each do |other_commit|
153
+ other_commit[:files].each do |file|
154
+ other_files << file unless files.include?(file)
155
+ end
156
+ end
157
+
158
+ # If we don't have enough other files, use some from sample data
159
+ if other_files.size < 3
160
+ sample_files = [
161
+ "src/main.js", "lib/utils.js", "css/styles.css", "README.md",
162
+ "package.json", "Dockerfile", ".github/workflows/ci.yml",
163
+ "src/components/Header.js", "app/models/user.rb", "config/database.yml"
164
+ ]
165
+ other_files += sample_files.reject { |f| files.include?(f) }
166
+ end
167
+
168
+ # Take up to 3 unique other files, prioritizing diverse ones
169
+ other_files = other_files.uniq.sample(3)
170
+
171
+ # Create options array with the correct answer and incorrect ones
172
+ all_options = ([correct_file] + other_files).shuffle
173
+
174
+ # Format the commit date nicely
175
+ nice_date = commit[:date].strftime('%b %d, %Y') rescue "Unknown date"
176
+
177
+ # Clean up commit message - take first line if multiple lines
178
+ message = commit[:message].to_s.split("\n").first.strip
179
+
180
+ # Format consistently with other mini-games
181
+ questions << {
182
+ question: "Which file was most likely changed in this commit?\n\n \"#{message}\"",
183
+ commit_info: "#{commit[:sha][0..6]} (#{nice_date})",
184
+ options: all_options,
185
+ correct_answer: correct_file
186
+ }
187
+ end
188
+
189
+ # Final safety check - if we couldn't create enough questions, use samples
190
+ if questions.size < self.class.questions_per_round
191
+ return generate_sample_questions
192
+ end
193
+
194
+ return questions
195
+ rescue => e
196
+ # If anything fails, fall back to sample questions
197
+ return generate_sample_questions
198
+ end
199
+ end
200
+
201
+ def evaluate_answers(question, player_answers)
202
+ results = {}
203
+
204
+ player_answers.each do |player_name, answer_data|
205
+ answered = answer_data[:answered] || false
206
+ player_answer = answer_data[:answer]
207
+ correct = player_answer == question[:correct_answer]
208
+
209
+ points = correct ? 10 : 0
210
+
211
+ # Bonus points for fast answers (if correct)
212
+ if correct
213
+ time_taken = answer_data[:time_taken] || 15
214
+
215
+ if time_taken < 5
216
+ points += 5
217
+ elsif time_taken < 10
218
+ points += 3
219
+ end
220
+ end
221
+
222
+ results[player_name] = {
223
+ answer: player_answer,
224
+ correct: correct,
225
+ points: points
226
+ }
227
+ end
228
+
229
+ results
230
+ end
231
+
232
+ # Generate sample questions only used when no repository data is available
233
+ def generate_sample_questions
234
+ questions = []
235
+
236
+ # Common file options
237
+ common_files = [
238
+ "src/main.js",
239
+ "README.md",
240
+ "lib/utils.js",
241
+ "css/styles.css"
242
+ ]
243
+
244
+ # Common sample commit messages
245
+ sample_messages = [
246
+ "Update documentation with new API endpoints",
247
+ "Fix styling issues in mobile view",
248
+ "Add error handling for network failures",
249
+ "Refactor authentication module for better performance",
250
+ "Update dependencies to latest versions"
251
+ ]
252
+
253
+ # Create different sample questions for variety
254
+ self.class.questions_per_round.times do |i|
255
+ # Use modulo to cycle through sample messages
256
+ message = sample_messages[i % sample_messages.size]
257
+
258
+ # Different correct answers for each question
259
+ correct_file = common_files[i % common_files.size]
260
+
261
+ # Options are all files with the correct one included
262
+ all_options = common_files.shuffle
263
+
264
+ # Create the question
265
+ questions << {
266
+ question: "Which file was most likely changed in this commit?\n\n \"#{message} (SAMPLE)\"",
267
+ commit_info: "sample#{i} (Demo Question)",
268
+ options: all_options,
269
+ correct_answer: correct_file
270
+ }
271
+ end
272
+
273
+ questions
274
+ end
275
+
276
+ # No private methods needed anymore
277
+ end
278
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: git_game_show
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Paulson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-03-05 00:00:00.000000000 Z
11
+ date: 2025-03-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -214,8 +214,8 @@ files:
214
214
  - lib/git_game_show/version.rb
215
215
  - mini_games/author_quiz.rb
216
216
  - mini_games/commit_message_completion.rb
217
- - mini_games/commit_message_quiz.rb
218
217
  - mini_games/date_ordering_quiz.rb
218
+ - mini_games/file_quiz.rb
219
219
  homepage: https://github.com/justinpaulson/git_game_show
220
220
  licenses:
221
221
  - MIT
@@ -1,589 +0,0 @@
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