git_game_show 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/git_game_show/game_server.rb +65 -17
- data/lib/git_game_show/mini_game.rb +12 -12
- data/lib/git_game_show/player_client.rb +42 -14
- data/lib/git_game_show/version.rb +1 -1
- data/mini_games/author_quiz.rb +45 -32
- data/mini_games/blame_game.rb +392 -0
- data/mini_games/branch_detective.rb +241 -0
- data/mini_games/commit_message_completion.rb +49 -36
- data/mini_games/date_ordering_quiz.rb +64 -48
- data/mini_games/file_quiz.rb +13 -0
- metadata +4 -2
@@ -0,0 +1,392 @@
|
|
1
|
+
module GitGameShow
|
2
|
+
# Enable this for testing/debugging
|
3
|
+
$BLAME_GAME_DEBUG = false
|
4
|
+
|
5
|
+
class BlameGame < MiniGame
|
6
|
+
self.name = "Blame Game"
|
7
|
+
self.description = "Identify who committed the highlighted line of code! (Can be slow to load)"
|
8
|
+
self.example = <<~EXAMPLE
|
9
|
+
Who committed the highlighted line of code?
|
10
|
+
|
11
|
+
File: main.rb
|
12
|
+
|
13
|
+
|
14
|
+
1: def initialize(options = {})
|
15
|
+
2: @logger = options[:logger] || Logger.new(STDOUT)
|
16
|
+
3: @config = load_configuration
|
17
|
+
\e[0;33;49m> 4: @connections = []\e[0m
|
18
|
+
5: @active = false
|
19
|
+
6: setup_signal_handlers
|
20
|
+
7: end
|
21
|
+
|
22
|
+
Choose your answer?
|
23
|
+
Bob Coder
|
24
|
+
Steve Developer
|
25
|
+
\e[0;32;49m> Alice Programmer\e[0m
|
26
|
+
Sandy Engineer
|
27
|
+
EXAMPLE
|
28
|
+
self.questions_per_round = 5
|
29
|
+
|
30
|
+
# Custom timing for this mini-game
|
31
|
+
def self.question_timeout
|
32
|
+
20 # 20 seconds per question (slightly longer than Author Quiz since more code to read)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.question_display_time
|
36
|
+
5 # 5 seconds between questions
|
37
|
+
end
|
38
|
+
|
39
|
+
def generate_questions(repo)
|
40
|
+
questions = []
|
41
|
+
|
42
|
+
# Check if repo exists and has commits
|
43
|
+
if repo.nil? || !repo.branches.size.positive?
|
44
|
+
puts "No repository with commits found, using sample questions" if $BLAME_GAME_DEBUG
|
45
|
+
return generate_sample_questions
|
46
|
+
end
|
47
|
+
|
48
|
+
begin
|
49
|
+
puts "Starting Blame Game question generation" if $BLAME_GAME_DEBUG
|
50
|
+
# Get all files in the repo
|
51
|
+
repo_path = repo.dir.path
|
52
|
+
|
53
|
+
# Get a more limited list of files tracked by git
|
54
|
+
# 1. Use git ls-files with specific extensions to limit scope
|
55
|
+
# 2. Exclude binary files and very large files right from the start
|
56
|
+
# 3. Limit to a reasonable number of files to consider
|
57
|
+
cmd = "cd #{repo_path} && git ls-files -- '*.rb' '*.js' '*.py' '*.html' '*.css' '*.ts' '*.jsx' '*.tsx' '*.md' '*.yml' '*.yaml' '*.json' '*.c' '*.cpp' '*.h' '*.java' | grep -v -E '\\.min\\.js$|\\.min\\.css$|\\.bundle\\.js$|\\.map$|\\.png$|\\.jpg$|\\.jpeg$|\\.gif$|\\.pdf$|\\.zip$|\\.tar$|\\.gz$|\\.jar$|\\.exe$|\\.bin$' | sort -R | head -100"
|
58
|
+
all_files = `#{cmd}`.split("\n").reject(&:empty?)
|
59
|
+
|
60
|
+
puts "Found #{all_files.size} files to choose from for Blame Game" if $BLAME_GAME_DEBUG
|
61
|
+
|
62
|
+
# Skip if no files found
|
63
|
+
if all_files.empty?
|
64
|
+
puts "No suitable files found in repository, using sample questions" if $BLAME_GAME_DEBUG
|
65
|
+
return generate_sample_questions
|
66
|
+
end
|
67
|
+
|
68
|
+
# Get all authors in advance to see if we have enough
|
69
|
+
all_authors_cmd = "cd #{repo_path} && git log --pretty=format:'%an' | sort | uniq"
|
70
|
+
all_authors = `#{all_authors_cmd}`.split("\n").uniq.reject(&:empty?)
|
71
|
+
|
72
|
+
puts "Found #{all_authors.size} unique authors in repository" if $BLAME_GAME_DEBUG
|
73
|
+
|
74
|
+
if all_authors.size < 2
|
75
|
+
puts "Not enough authors found (need at least 2), using sample questions" if $BLAME_GAME_DEBUG
|
76
|
+
return generate_sample_questions
|
77
|
+
end
|
78
|
+
|
79
|
+
# Try to generate the requested number of questions
|
80
|
+
# More attempts to increase chances of getting real data
|
81
|
+
attempts = 0
|
82
|
+
max_attempts = self.class.questions_per_round * 10
|
83
|
+
|
84
|
+
puts "Starting question generation process, will try up to #{max_attempts} attempts" if $BLAME_GAME_DEBUG
|
85
|
+
|
86
|
+
while questions.size < self.class.questions_per_round && attempts < max_attempts
|
87
|
+
attempts += 1
|
88
|
+
|
89
|
+
# Select a random file
|
90
|
+
file_path = all_files.sample
|
91
|
+
|
92
|
+
# Skip if file path is invalid or file doesn't exist
|
93
|
+
full_path = File.join(repo_path, file_path)
|
94
|
+
next unless File.exist?(full_path)
|
95
|
+
|
96
|
+
# Skip very large files (over 500 KB)
|
97
|
+
next if File.size(full_path) > 500_000
|
98
|
+
|
99
|
+
# Skip binary or non-text files
|
100
|
+
begin
|
101
|
+
# Test if this file can be read as text
|
102
|
+
File.read(full_path, 100).encode('UTF-8', :invalid => :replace, :undef => :replace)
|
103
|
+
rescue
|
104
|
+
next
|
105
|
+
end
|
106
|
+
|
107
|
+
# Skip checking line count, which is slow for large files
|
108
|
+
# Instead read the first chunk to estimate if it's large enough
|
109
|
+
content_preview = File.read(full_path, 1000) rescue nil
|
110
|
+
next unless content_preview
|
111
|
+
|
112
|
+
# Rough estimate if file has enough lines by checking for newlines
|
113
|
+
newline_count = content_preview.count("\n")
|
114
|
+
next if newline_count < 7
|
115
|
+
|
116
|
+
# Estimate total lines - if preview has enough lines, we can use that section
|
117
|
+
if newline_count >= 20
|
118
|
+
# Use lines from the preview which we already have
|
119
|
+
lines = content_preview.split("\n")
|
120
|
+
# Choose a line not too close to the beginning or end
|
121
|
+
target_idx = rand(4..(lines.size - 5))
|
122
|
+
# Content is just the section we already read, no need to read the whole file
|
123
|
+
file_content = lines
|
124
|
+
# Target line number is relative to the preview
|
125
|
+
target_line_num = target_idx + 1 # 1-based line numbers
|
126
|
+
# For display purposes, show the real line number
|
127
|
+
target_line_display = target_line_num
|
128
|
+
else
|
129
|
+
# For small files, do a quick line count
|
130
|
+
# This is much faster than counting all lines in a large file
|
131
|
+
line_count = content_preview.count("\n") + 1
|
132
|
+
file_content = File.readlines(full_path) rescue nil
|
133
|
+
next unless file_content
|
134
|
+
target_line_num = rand(4..(line_count - 4))
|
135
|
+
target_line_display = target_line_num
|
136
|
+
end
|
137
|
+
|
138
|
+
# Get git blame for the selected line with better error checking
|
139
|
+
# Use a more efficient git blame approach - only get what we need
|
140
|
+
begin
|
141
|
+
# Extract both the author name and commit date
|
142
|
+
blame_cmd = "cd #{repo_path} && git blame -L #{target_line_num},#{target_line_num} --line-porcelain #{file_path}"
|
143
|
+
blame_output = `#{blame_cmd}`
|
144
|
+
|
145
|
+
# Extract author name
|
146
|
+
author_name_match = blame_output.match(/^author (.+)$/)
|
147
|
+
author_name = author_name_match ? author_name_match[1].strip : ""
|
148
|
+
|
149
|
+
# Extract commit date (author-time and author-tz)
|
150
|
+
author_time_match = blame_output.match(/^author-time (\d+)$/)
|
151
|
+
author_tz_match = blame_output.match(/^author-tz (.+)$/)
|
152
|
+
|
153
|
+
commit_date = nil
|
154
|
+
if author_time_match
|
155
|
+
timestamp = author_time_match[1].to_i
|
156
|
+
commit_date = Time.at(timestamp).strftime("%Y-%m-%d")
|
157
|
+
end
|
158
|
+
|
159
|
+
# Skip if we couldn't get a valid author
|
160
|
+
if author_name.empty? || author_name.include?("fatal:") || author_name == "Not Committed Yet"
|
161
|
+
puts "Skipping uncommitted or invalid line" if $BLAME_GAME_DEBUG
|
162
|
+
next
|
163
|
+
end
|
164
|
+
|
165
|
+
puts "Found line committed by: #{author_name} on #{commit_date}" if $BLAME_GAME_DEBUG
|
166
|
+
|
167
|
+
# We already have all authors, so we don't need to fetch them again
|
168
|
+
# But we should still check if we have enough authors to make this challenging
|
169
|
+
if all_authors.size < 4 # Need at least 3 incorrect options + 1 correct
|
170
|
+
puts "Not enough unique authors in the repository: #{all_authors.size}" if $BLAME_GAME_DEBUG
|
171
|
+
next
|
172
|
+
end
|
173
|
+
rescue => e
|
174
|
+
puts "Error getting blame info: #{e.message}" if $BLAME_GAME_DEBUG
|
175
|
+
next
|
176
|
+
end
|
177
|
+
|
178
|
+
# Get context lines (3 before, target line, 3 after)
|
179
|
+
# Use the content we already read if possible, otherwise read from file
|
180
|
+
if newline_count >= 20 && target_idx >= 3 && target_idx <= (lines.size - 4)
|
181
|
+
# We can use the lines we already have from the preview
|
182
|
+
context_lines = lines[(target_idx-3)..(target_idx+3)]
|
183
|
+
start_idx = target_idx - 3
|
184
|
+
else
|
185
|
+
# Get relevant content directly - much more efficient than reading entire file
|
186
|
+
context_cmd = "cd #{repo_path} && tail -n +#{[target_line_num-3, 1].max} #{file_path} | head -7"
|
187
|
+
context_output = `#{context_cmd}`
|
188
|
+
context_lines = context_output.split("\n")
|
189
|
+
start_idx = [target_line_num - 4, 0].max
|
190
|
+
end
|
191
|
+
|
192
|
+
next unless context_lines && context_lines.size > 0
|
193
|
+
|
194
|
+
# Add line indicators for display
|
195
|
+
display_lines = []
|
196
|
+
context_lines.each_with_index do |line, idx|
|
197
|
+
line_num = start_idx + idx + 1
|
198
|
+
|
199
|
+
# Mark the target line with > prefix
|
200
|
+
prefix = (line_num == target_line_num) ? "> " : " "
|
201
|
+
|
202
|
+
# Clean the line for display (handle tab characters, trim long lines)
|
203
|
+
clean_line = line.gsub("\t", " ").rstrip
|
204
|
+
if clean_line.length > 100
|
205
|
+
clean_line = clean_line[0..97] + "..."
|
206
|
+
end
|
207
|
+
|
208
|
+
display_lines << "#{prefix}#{line_num}: #{clean_line}"
|
209
|
+
end
|
210
|
+
|
211
|
+
# Create context string with file path and highlighted lines
|
212
|
+
context = "File: #{file_path}\n\n#{display_lines.join("\n")}"
|
213
|
+
|
214
|
+
# Get incorrect options (other authors)
|
215
|
+
incorrect_authors = all_authors.reject { |a| a == author_name }.shuffle.take(3)
|
216
|
+
|
217
|
+
# If we don't have enough distinct authors, generate some
|
218
|
+
if incorrect_authors.size < 3
|
219
|
+
sample_authors = ["Alice", "Bob", "Charlie", "David", "Emma"].reject { |a| a == author_name }
|
220
|
+
incorrect_authors += sample_authors.take(3 - incorrect_authors.size)
|
221
|
+
end
|
222
|
+
|
223
|
+
# Create options array with the correct answer and incorrect ones
|
224
|
+
all_options = ([author_name] + incorrect_authors).shuffle
|
225
|
+
|
226
|
+
# Create the question with date information
|
227
|
+
question_text = "Who committed the highlighted line of code"
|
228
|
+
question_text += commit_date ? " on #{commit_date}?" : "?"
|
229
|
+
|
230
|
+
questions << {
|
231
|
+
question: question_text,
|
232
|
+
context: context,
|
233
|
+
options: all_options,
|
234
|
+
correct_answer: author_name
|
235
|
+
}
|
236
|
+
end
|
237
|
+
|
238
|
+
rescue => e
|
239
|
+
# Silently fail and use sample questions instead
|
240
|
+
return generate_sample_questions
|
241
|
+
end
|
242
|
+
|
243
|
+
# Make sure we have enough questions or use fallback
|
244
|
+
if questions.size < self.class.questions_per_round
|
245
|
+
puts "Could only generate #{questions.size} questions, using sample questions to fill the rest" if $BLAME_GAME_DEBUG
|
246
|
+
sample_questions = generate_sample_questions
|
247
|
+
# Add enough sample questions to reach the required number
|
248
|
+
questions += sample_questions[0...(self.class.questions_per_round - questions.size)]
|
249
|
+
end
|
250
|
+
|
251
|
+
puts "Generated #{questions.size} questions for Blame Game" if $BLAME_GAME_DEBUG
|
252
|
+
questions
|
253
|
+
rescue => e
|
254
|
+
puts "Error in BlameGame#generate_questions: #{e.message}\n#{e.backtrace.join("\n")}" if $BLAME_GAME_DEBUG
|
255
|
+
generate_sample_questions
|
256
|
+
end
|
257
|
+
|
258
|
+
def evaluate_answers(question, player_answers)
|
259
|
+
results = {}
|
260
|
+
|
261
|
+
player_answers.each do |player_name, answer_data|
|
262
|
+
answered = answer_data[:answered] || false
|
263
|
+
player_answer = answer_data[:answer]
|
264
|
+
correct = player_answer == question[:correct_answer]
|
265
|
+
|
266
|
+
points = correct ? 10 : 0
|
267
|
+
|
268
|
+
# Bonus points for fast answers (if correct)
|
269
|
+
if correct
|
270
|
+
time_taken = answer_data[:time_taken] || self.class.question_timeout
|
271
|
+
|
272
|
+
if time_taken < 5
|
273
|
+
points += 5
|
274
|
+
elsif time_taken < 10
|
275
|
+
points += 3
|
276
|
+
elsif time_taken < 15
|
277
|
+
points += 1
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
results[player_name] = {
|
282
|
+
answer: player_answer,
|
283
|
+
correct: correct,
|
284
|
+
points: points
|
285
|
+
}
|
286
|
+
end
|
287
|
+
|
288
|
+
results
|
289
|
+
end
|
290
|
+
|
291
|
+
def generate_sample_questions
|
292
|
+
# Create sample questions in case the repo doesn't have enough data
|
293
|
+
sample_authors = ["Alice", "Bob", "Charlie", "David", "Emma"]
|
294
|
+
sample_files = ["main.rb", "utils.js", "config.py", "index.html", "styles.css"]
|
295
|
+
# Sample dates for the commits
|
296
|
+
sample_dates = ["2024-01-15", "2024-02-20", "2024-03-05", "2024-03-10", "2024-03-11"]
|
297
|
+
|
298
|
+
questions = []
|
299
|
+
|
300
|
+
self.class.questions_per_round.times do |i|
|
301
|
+
# Select a random author, file, and date for this sample
|
302
|
+
correct_author = sample_authors.sample
|
303
|
+
file_name = sample_files.sample
|
304
|
+
commit_date = sample_dates.sample
|
305
|
+
|
306
|
+
# Create sample code context
|
307
|
+
context_lines = []
|
308
|
+
|
309
|
+
case file_name
|
310
|
+
when "main.rb"
|
311
|
+
context_lines = [
|
312
|
+
"def initialize(options = {})",
|
313
|
+
" @logger = options[:logger] || Logger.new(STDOUT)",
|
314
|
+
" @config = load_configuration",
|
315
|
+
" @connections = []",
|
316
|
+
" @active = false",
|
317
|
+
" setup_signal_handlers",
|
318
|
+
"end"
|
319
|
+
]
|
320
|
+
when "utils.js"
|
321
|
+
context_lines = [
|
322
|
+
"function formatDate(date) {",
|
323
|
+
" const day = String(date.getDate()).padStart(2, '0');",
|
324
|
+
" const month = String(date.getMonth() + 1).padStart(2, '0');",
|
325
|
+
" const year = date.getFullYear();",
|
326
|
+
" return `${year}-${month}-${day}`;",
|
327
|
+
" // TODO: Add support for different formats",
|
328
|
+
"};"
|
329
|
+
]
|
330
|
+
when "config.py"
|
331
|
+
context_lines = [
|
332
|
+
"class Config:",
|
333
|
+
" DEBUG = False",
|
334
|
+
" TESTING = False",
|
335
|
+
" DATABASE_URI = os.environ.get('DATABASE_URI')",
|
336
|
+
" SECRET_KEY = os.environ.get('SECRET_KEY')",
|
337
|
+
" SESSION_COOKIE_SECURE = True",
|
338
|
+
" TEMPLATES_AUTO_RELOAD = True"
|
339
|
+
]
|
340
|
+
when "index.html"
|
341
|
+
context_lines = [
|
342
|
+
"<header>",
|
343
|
+
" <nav>",
|
344
|
+
" <ul>",
|
345
|
+
" <li><a href=\"/\">Home</a></li>",
|
346
|
+
" <li><a href=\"/about\">About</a></li>",
|
347
|
+
" <li><a href=\"/contact\">Contact</a></li>",
|
348
|
+
" </ul>"
|
349
|
+
]
|
350
|
+
when "styles.css"
|
351
|
+
context_lines = [
|
352
|
+
"body {",
|
353
|
+
" font-family: 'Helvetica', sans-serif;",
|
354
|
+
" line-height: 1.6;",
|
355
|
+
" color: #333;",
|
356
|
+
" margin: 0;",
|
357
|
+
" padding: 20px;",
|
358
|
+
" background-color: #f5f5f5;"
|
359
|
+
]
|
360
|
+
end
|
361
|
+
|
362
|
+
# Mark a random line as the target (line 4 is index 3)
|
363
|
+
target_idx = 3 # Middle line
|
364
|
+
|
365
|
+
# Add line numbers and indicators
|
366
|
+
display_lines = []
|
367
|
+
context_lines.each_with_index do |line, idx|
|
368
|
+
line_num = idx + 1
|
369
|
+
prefix = (idx == target_idx) ? "> " : " "
|
370
|
+
display_lines << "#{prefix}#{line_num}: #{line}"
|
371
|
+
end
|
372
|
+
|
373
|
+
# Create context string
|
374
|
+
context = "File: #{file_name} (SAMPLE)\n\n#{display_lines.join("\n")}"
|
375
|
+
|
376
|
+
# Get incorrect options (other authors)
|
377
|
+
incorrect_authors = sample_authors.reject { |a| a == correct_author }.sample(3)
|
378
|
+
all_options = ([correct_author] + incorrect_authors).shuffle
|
379
|
+
|
380
|
+
# Create the sample question with date
|
381
|
+
questions << {
|
382
|
+
question: "Who committed the highlighted line of code on #{commit_date}? (SAMPLE)",
|
383
|
+
context: context,
|
384
|
+
options: all_options,
|
385
|
+
correct_answer: correct_author
|
386
|
+
}
|
387
|
+
end
|
388
|
+
|
389
|
+
questions
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
@@ -0,0 +1,241 @@
|
|
1
|
+
module GitGameShow
|
2
|
+
class BranchDetective < MiniGame
|
3
|
+
self.name = "Branch Detective"
|
4
|
+
self.description = "Identify which branch a commit belongs to!"
|
5
|
+
self.example = <<~EXAMPLE
|
6
|
+
Which branch was this commit originally made on?
|
7
|
+
|
8
|
+
"Add pagination to users list"
|
9
|
+
|
10
|
+
abcdef7 (by Jane Developer on 2023-05-20 16:45:12)
|
11
|
+
|
12
|
+
Choose your answer:
|
13
|
+
development
|
14
|
+
\e[0;32;49m> feature/user-management\e[0m
|
15
|
+
main
|
16
|
+
hotfix/login-issue
|
17
|
+
EXAMPLE
|
18
|
+
self.questions_per_round = 5
|
19
|
+
|
20
|
+
# Custom timing for this mini-game
|
21
|
+
def self.question_timeout
|
22
|
+
15 # 15 seconds per question
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.question_display_time
|
26
|
+
5 # 5 seconds between questions
|
27
|
+
end
|
28
|
+
|
29
|
+
def generate_questions(repo)
|
30
|
+
@repo = repo
|
31
|
+
begin
|
32
|
+
# Get all branches (both local and remote)
|
33
|
+
branches = {}
|
34
|
+
|
35
|
+
# Use Git command to get all local and remote branches
|
36
|
+
|
37
|
+
# get all remote branches from the git repository
|
38
|
+
all_remotes_cmd = "cd #{@repo.dir.path} && git branch -r"
|
39
|
+
all_branch_output = `#{all_remotes_cmd}`
|
40
|
+
|
41
|
+
# Parse branch names and clean them up
|
42
|
+
branch_names = all_branch_output.split("\n").map do |branch|
|
43
|
+
branch = branch.gsub(/^\* /, '').strip # Remove the * prefix from current branch
|
44
|
+
|
45
|
+
# Skip special branches like HEAD
|
46
|
+
next if branch == 'HEAD' || branch =~ /HEAD detached/
|
47
|
+
|
48
|
+
branch
|
49
|
+
end.compact.uniq # Remove nils and duplicates
|
50
|
+
|
51
|
+
# Filter out any empty branch names
|
52
|
+
branch_names.reject!(&:empty?)
|
53
|
+
|
54
|
+
# Need at least 3 branches to make interesting questions
|
55
|
+
if branch_names.size < 5
|
56
|
+
return generate_sample_questions
|
57
|
+
end
|
58
|
+
|
59
|
+
branch_names = branch_names.sample(100) if branch_names.size > 100
|
60
|
+
|
61
|
+
branch_names.each do |branch|
|
62
|
+
# Get commits for this branch
|
63
|
+
branches[branch] = get_commits_for_branch(branch)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Generate questions
|
67
|
+
questions = []
|
68
|
+
|
69
|
+
self.class.questions_per_round.times do
|
70
|
+
# Select branches that have commits
|
71
|
+
branch_options = branch_names.sample(4)
|
72
|
+
|
73
|
+
|
74
|
+
# If we don't have 4 valid branches, pad with duplicates and ensure uniqueness later
|
75
|
+
if branch_options.size < 4
|
76
|
+
branch_options = branch_names.dup
|
77
|
+
while branch_options.size < 4
|
78
|
+
branch_options << branch_names.sample
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Choose a random branch as the correct answer
|
83
|
+
correct_branch = branch_options.sample
|
84
|
+
|
85
|
+
# Choose a random commit from this branch
|
86
|
+
commit = branches[correct_branch].sample
|
87
|
+
|
88
|
+
# Create the question
|
89
|
+
commit_date = commit[:date] #.split(" ")[0..2].join(" ") + " " + commit[:date].split(" ")[4]
|
90
|
+
commit_short_sha = commit[:sha][0..6]
|
91
|
+
|
92
|
+
# Format the commit message for display
|
93
|
+
message = commit[:message].lines.first&.strip || "No message"
|
94
|
+
message = message.length > 50 ? "#{message[0...47]}..." : message
|
95
|
+
|
96
|
+
questions << {
|
97
|
+
question: "Which branch was this commit originally made on?\n\n \"#{message}\"",
|
98
|
+
commit_info: "#{commit_short_sha} (by #{commit[:author]} on #{commit_date})",
|
99
|
+
options: branch_options.uniq.shuffle,
|
100
|
+
correct_answer: correct_branch
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
# If we couldn't generate enough questions, fill with sample questions
|
105
|
+
if questions.size < self.class.questions_per_round
|
106
|
+
sample_questions = generate_sample_questions
|
107
|
+
questions += sample_questions[0...(self.class.questions_per_round - questions.size)]
|
108
|
+
end
|
109
|
+
|
110
|
+
questions
|
111
|
+
rescue => e
|
112
|
+
# If any errors occur, fall back to sample questions
|
113
|
+
generate_sample_questions
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Generate sample questions
|
118
|
+
def generate_sample_questions
|
119
|
+
questions = []
|
120
|
+
|
121
|
+
# Sample data with branch names and commits
|
122
|
+
sample_branches = [
|
123
|
+
"main", "develop", "feature/user-auth", "bugfix/login",
|
124
|
+
"feature/payment", "hotfix/security", "release/v2.0", "staging"
|
125
|
+
]
|
126
|
+
|
127
|
+
# Sample commit data
|
128
|
+
sample_commits = [
|
129
|
+
{ message: "[SAMPLE] Add user authentication flow", author: "Jane Doe", date: "2023-05-15 14:30:22", sha: "a1b2c3d" },
|
130
|
+
{ message: "[SAMPLE] Fix login page styling issues", author: "John Smith", date: "2023-05-18 10:15:45", sha: "e4f5g6h" },
|
131
|
+
{ message: "[SAMPLE] Implement password reset functionality", author: "Alice Johnson", date: "2023-05-20 16:45:12", sha: "i7j8k9l" },
|
132
|
+
{ message: "[SAMPLE] Add payment gateway integration", author: "Bob Brown", date: "2023-05-22 09:20:33", sha: "m2n3o4p" },
|
133
|
+
{ message: "[SAMPLE] Update README with API documentation", author: "Charlie Davis", date: "2023-05-25 11:05:56", sha: "q5r6s7t" }
|
134
|
+
]
|
135
|
+
|
136
|
+
# Generate sample questions
|
137
|
+
self.class.questions_per_round.times do |i|
|
138
|
+
# Select a random commit
|
139
|
+
commit = sample_commits[i % sample_commits.size]
|
140
|
+
|
141
|
+
# Select 4 random branch names
|
142
|
+
branch_options = sample_branches.sample(4)
|
143
|
+
|
144
|
+
# Choose a correct branch
|
145
|
+
correct_branch = branch_options.sample
|
146
|
+
|
147
|
+
questions << {
|
148
|
+
question: "Which branch was this commit originally made on?\n\n \"#{commit[:message]}\"",
|
149
|
+
commit_info: "#{commit[:sha]} (by #{commit[:author]} on #{commit[:date]})",
|
150
|
+
options: branch_options,
|
151
|
+
correct_answer: correct_branch
|
152
|
+
}
|
153
|
+
end
|
154
|
+
|
155
|
+
questions
|
156
|
+
end
|
157
|
+
|
158
|
+
def evaluate_answers(question, player_answers)
|
159
|
+
results = {}
|
160
|
+
|
161
|
+
player_answers.each do |player_name, answer_data|
|
162
|
+
answered = answer_data[:answered] || false
|
163
|
+
player_answer = answer_data[:answer]
|
164
|
+
time_taken = answer_data[:time_taken] || self.class.question_timeout
|
165
|
+
|
166
|
+
# Check if the answer is correct
|
167
|
+
correct = player_answer == question[:correct_answer]
|
168
|
+
|
169
|
+
# Calculate points
|
170
|
+
points = 0
|
171
|
+
|
172
|
+
# Base points for correct answer
|
173
|
+
if correct
|
174
|
+
points = 10
|
175
|
+
|
176
|
+
# Bonus points for fast answers
|
177
|
+
if time_taken < 5
|
178
|
+
points += 5 # Very fast (under 5 seconds)
|
179
|
+
elsif time_taken < 10
|
180
|
+
points += 3 # Pretty fast (under 10 seconds)
|
181
|
+
elsif time_taken < 12
|
182
|
+
points += 1 # Somewhat fast (under 12 seconds)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Store the results
|
187
|
+
results[player_name] = {
|
188
|
+
answer: player_answer,
|
189
|
+
correct: correct,
|
190
|
+
points: points
|
191
|
+
}
|
192
|
+
end
|
193
|
+
|
194
|
+
results
|
195
|
+
end
|
196
|
+
|
197
|
+
private
|
198
|
+
|
199
|
+
def get_commits_for_branch branch
|
200
|
+
unique_commits = []
|
201
|
+
|
202
|
+
# Try different ways to reference the branch
|
203
|
+
begin
|
204
|
+
# Try a few different ways to reference the branch
|
205
|
+
got_commits = false
|
206
|
+
|
207
|
+
# First try as a local branch
|
208
|
+
cmd = "cd #{@repo.dir.path} && git log --pretty '#{branch}' --max-count=5 2>/dev/null"
|
209
|
+
commit_output = `#{cmd}`
|
210
|
+
|
211
|
+
# Process commits if we found any
|
212
|
+
commits = commit_output.split("commit ")[1..-1]
|
213
|
+
|
214
|
+
commits.each do |commit|
|
215
|
+
|
216
|
+
# Extract commit info
|
217
|
+
sha = commit.lines[0].split(" ")[0].strip
|
218
|
+
author = commit.lines[1].gsub("Author: ", "").split("<")[0].strip
|
219
|
+
date = commit.lines[2].gsub("Date: ", "").strip
|
220
|
+
message = commit.lines[4..-1].join("\n")
|
221
|
+
|
222
|
+
next unless message.length > 10
|
223
|
+
next if message.include?("Merge pull request")
|
224
|
+
# Store this commit info
|
225
|
+
unique_commits << {
|
226
|
+
sha: sha,
|
227
|
+
message: message,
|
228
|
+
author: author,
|
229
|
+
date: date
|
230
|
+
}
|
231
|
+
end
|
232
|
+
|
233
|
+
rescue => e
|
234
|
+
# If we hit any errors with this branch, just skip it
|
235
|
+
[]
|
236
|
+
end
|
237
|
+
|
238
|
+
unique_commits
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|