github-daily-digest 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.
- checksums.yaml +7 -0
- data/README.md +35 -0
- data/Rakefile +4 -0
- data/bin/console +11 -0
- data/bin/github-daily-digest +140 -0
- data/bin/setup +8 -0
- data/github-daily-digest.gemspec +47 -0
- data/github-daily-digest.rb +20 -0
- data/lib/activity_analyzer.rb +48 -0
- data/lib/configuration.rb +260 -0
- data/lib/daily_digest_runner.rb +932 -0
- data/lib/gemini_service.rb +616 -0
- data/lib/github-daily-digest/version.rb +5 -0
- data/lib/github_daily_digest.rb +16 -0
- data/lib/github_graphql_service.rb +1191 -0
- data/lib/github_service.rb +364 -0
- data/lib/html_formatter.rb +1297 -0
- data/lib/language_analyzer.rb +163 -0
- data/lib/markdown_formatter.rb +137 -0
- data/lib/output_formatter.rb +818 -0
- metadata +178 -0
@@ -0,0 +1,818 @@
|
|
1
|
+
module GithubDailyDigest
|
2
|
+
class OutputFormatter
|
3
|
+
def initialize(config:, logger:)
|
4
|
+
@config = config
|
5
|
+
@logger = logger
|
6
|
+
end
|
7
|
+
|
8
|
+
def format(results, format_type = nil)
|
9
|
+
# If a specific format_type is provided, use that
|
10
|
+
# Otherwise, use the first format from the config
|
11
|
+
output_format = format_type || (@config.output_formats&.first || 'json')
|
12
|
+
|
13
|
+
case output_format
|
14
|
+
when 'json'
|
15
|
+
format_as_json(results)
|
16
|
+
when 'markdown'
|
17
|
+
format_as_markdown(results)
|
18
|
+
when 'html'
|
19
|
+
# HTML formatting is handled by HtmlFormatter but we need to recognize it as valid
|
20
|
+
format_as_json(results) # Return JSON data that HtmlFormatter will use
|
21
|
+
else
|
22
|
+
@logger.warn("Unknown output format: #{output_format}, defaulting to JSON")
|
23
|
+
format_as_json(results)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def format_as_json(results)
|
30
|
+
JSON.pretty_generate(results)
|
31
|
+
end
|
32
|
+
|
33
|
+
def format_as_markdown(results)
|
34
|
+
# Create a very simplistic markdown output for robustness
|
35
|
+
markdown = "# GitHub Activity Digest\n\n"
|
36
|
+
|
37
|
+
# Extract generation time or use current time
|
38
|
+
generated_at = if results.is_a?(Hash) && results[:_meta].is_a?(Hash) && results[:_meta][:generated_at].is_a?(String)
|
39
|
+
results[:_meta][:generated_at]
|
40
|
+
else
|
41
|
+
Time.now.strftime("%Y-%m-%d %H:%M:%S")
|
42
|
+
end
|
43
|
+
|
44
|
+
markdown << "Generated on: #{generated_at}\n\n"
|
45
|
+
|
46
|
+
# Add summary statistics section if available
|
47
|
+
if results.is_a?(Hash) && (results["summary_statistics"].is_a?(Hash) || results[:summary_statistics].is_a?(Hash))
|
48
|
+
stats = results["summary_statistics"] || results[:summary_statistics]
|
49
|
+
|
50
|
+
markdown << "## Summary Statistics\n\n"
|
51
|
+
|
52
|
+
# Add AI summary if available
|
53
|
+
if stats["ai_summary"].is_a?(String)
|
54
|
+
markdown << "> #{stats["ai_summary"]}\n\n"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Create a summary statistics table
|
58
|
+
markdown << "| Metric | Value |\n"
|
59
|
+
markdown << "| --- | --- |\n"
|
60
|
+
markdown << "| **Time Period** | #{stats["period"] || "Last 7 days"} |\n"
|
61
|
+
markdown << "| **Total Commits** | #{stats["total_commits"] || 0} |\n"
|
62
|
+
markdown << "| **Total Lines Changed** | #{stats["total_lines_changed"] || 0} |\n"
|
63
|
+
markdown << "| **Active Developers** | #{stats["active_users_count"] || 0} |\n"
|
64
|
+
markdown << "| **Active Repositories** | #{stats["active_repos_count"] || 0} |\n"
|
65
|
+
end
|
66
|
+
|
67
|
+
# Add active users section
|
68
|
+
markdown << "\n## Active Users\n\n"
|
69
|
+
|
70
|
+
# Collect all active users from all organizations and merge by username
|
71
|
+
merged_users = {}
|
72
|
+
|
73
|
+
if results.is_a?(Hash)
|
74
|
+
results.each do |org_name, org_data|
|
75
|
+
next if org_name == :_meta || org_name == "_meta" || org_name == "summary_statistics" || !org_data.is_a?(Hash)
|
76
|
+
|
77
|
+
# Get users who have activity
|
78
|
+
org_data.each do |username, user_data|
|
79
|
+
next if username == "_meta" || username == :_meta || !user_data.is_a?(Hash)
|
80
|
+
|
81
|
+
# Check if user has any meaningful activity
|
82
|
+
has_activity = false
|
83
|
+
|
84
|
+
# Check if user has commits
|
85
|
+
if user_data["commits"].is_a?(Array) && !user_data["commits"].empty?
|
86
|
+
has_activity = true
|
87
|
+
end
|
88
|
+
|
89
|
+
# Check for various activity indicators
|
90
|
+
has_activity ||= user_data["commits_count"].to_i > 0 if user_data["commits_count"]
|
91
|
+
has_activity ||= user_data["commit_count"].to_i > 0 if user_data["commit_count"]
|
92
|
+
has_activity ||= user_data["prs_count"].to_i > 0 if user_data["prs_count"]
|
93
|
+
has_activity ||= user_data["pr_count"].to_i > 0 if user_data["pr_count"]
|
94
|
+
has_activity ||= user_data["lines_changed"].to_i > 0 if user_data["lines_changed"]
|
95
|
+
|
96
|
+
if has_activity
|
97
|
+
# Initialize merged user if doesn't exist
|
98
|
+
unless merged_users[username]
|
99
|
+
merged_users[username] = {
|
100
|
+
username: username,
|
101
|
+
lines_changed: 0,
|
102
|
+
total_score: 0,
|
103
|
+
contribution_weights: {
|
104
|
+
"lines_of_code" => 0,
|
105
|
+
"complexity" => 0,
|
106
|
+
"technical_depth" => 0,
|
107
|
+
"scope" => 0,
|
108
|
+
"pr_reviews" => 0
|
109
|
+
},
|
110
|
+
organizations: [],
|
111
|
+
org_details: {}
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
# Add this organization to the list
|
116
|
+
org_details = merged_users[username][:org_details][org_name] = {
|
117
|
+
data: user_data,
|
118
|
+
lines_changed: user_data["lines_changed"].to_i
|
119
|
+
}
|
120
|
+
|
121
|
+
# Add organization to list if not present
|
122
|
+
unless merged_users[username][:organizations].include?(org_name)
|
123
|
+
merged_users[username][:organizations] << org_name
|
124
|
+
end
|
125
|
+
|
126
|
+
# Add lines changed
|
127
|
+
merged_users[username][:lines_changed] += user_data["lines_changed"].to_i
|
128
|
+
|
129
|
+
# Use highest score
|
130
|
+
user_score = user_data["total_score"].to_i
|
131
|
+
if user_score > merged_users[username][:total_score]
|
132
|
+
merged_users[username][:total_score] = user_score
|
133
|
+
end
|
134
|
+
|
135
|
+
# Use highest contribution weights
|
136
|
+
if user_data["contribution_weights"].is_a?(Hash)
|
137
|
+
weights = user_data["contribution_weights"]
|
138
|
+
["lines_of_code", "complexity", "technical_depth", "scope", "pr_reviews"].each do |key|
|
139
|
+
weight_value = weights[key].to_i rescue 0
|
140
|
+
if weight_value > merged_users[username][:contribution_weights][key]
|
141
|
+
merged_users[username][:contribution_weights][key] = weight_value
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
active_users = merged_users.values
|
151
|
+
|
152
|
+
if active_users.empty?
|
153
|
+
markdown << "No active users found in the specified time period.\n\n"
|
154
|
+
else
|
155
|
+
# Create a table of active users with scores
|
156
|
+
markdown << "| Username | Organizations | Lines Changed | Contribution Score | Code | Complexity | Tech Depth | Scope | Reviews |\n"
|
157
|
+
markdown << "| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n"
|
158
|
+
|
159
|
+
active_users.sort_by { |u| -1 * u[:total_score] }.each do |user|
|
160
|
+
username = user[:username]
|
161
|
+
orgs = user[:organizations].join(", ")
|
162
|
+
lines_changed = user[:lines_changed]
|
163
|
+
score = user[:total_score]
|
164
|
+
|
165
|
+
# Extract contribution weights
|
166
|
+
weights = user[:contribution_weights]
|
167
|
+
code_weight = weights["lines_of_code"].to_i
|
168
|
+
complexity_weight = weights["complexity"].to_i
|
169
|
+
tech_depth_weight = weights["technical_depth"].to_i
|
170
|
+
scope_weight = weights["scope"].to_i
|
171
|
+
reviews_weight = weights["pr_reviews"].to_i
|
172
|
+
|
173
|
+
markdown << "| #{username} | #{orgs} | #{lines_changed} | #{score} | #{code_weight} | #{complexity_weight} | #{tech_depth_weight} | #{scope_weight} | #{reviews_weight} |\n"
|
174
|
+
end
|
175
|
+
|
176
|
+
markdown << "\n"
|
177
|
+
|
178
|
+
# Add detailed breakdown for each user
|
179
|
+
markdown << "## User Activity Details\n\n"
|
180
|
+
|
181
|
+
active_users.sort_by { |u| -1 * u[:total_score] }.each do |user|
|
182
|
+
username = user[:username]
|
183
|
+
orgs = user[:organizations].join(", ")
|
184
|
+
|
185
|
+
markdown << "### #{username} (#{orgs})\n\n"
|
186
|
+
|
187
|
+
# Collect summaries from all organizations
|
188
|
+
summaries = []
|
189
|
+
repositories = []
|
190
|
+
|
191
|
+
user[:organizations].each do |org_name|
|
192
|
+
org_data = user[:org_details][org_name]
|
193
|
+
next unless org_data
|
194
|
+
|
195
|
+
# Add user summary if available
|
196
|
+
if org_data[:data]["summary"].is_a?(String) && !org_data[:data]["summary"].empty?
|
197
|
+
summaries << org_data[:data]["summary"]
|
198
|
+
end
|
199
|
+
|
200
|
+
# Collect repositories
|
201
|
+
if org_data[:data]["projects"].is_a?(Array)
|
202
|
+
org_data[:data]["projects"].each do |project|
|
203
|
+
if project.is_a?(Hash)
|
204
|
+
repo_name = project["name"] || project[:name] || "Unknown repository"
|
205
|
+
commits = project["commits"] || project[:commits] || 0
|
206
|
+
changes = project["lines_changed"] || project[:lines_changed] || 0
|
207
|
+
|
208
|
+
repositories << {
|
209
|
+
name: repo_name,
|
210
|
+
org: org_name,
|
211
|
+
commits: commits,
|
212
|
+
changes: changes
|
213
|
+
}
|
214
|
+
else
|
215
|
+
# Handle case where project is just a string with repo name
|
216
|
+
repo_name = project.to_s
|
217
|
+
repositories << {
|
218
|
+
name: repo_name,
|
219
|
+
org: org_name,
|
220
|
+
commits: 0,
|
221
|
+
changes: 0
|
222
|
+
}
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Output the summary if available
|
229
|
+
if summaries.any?
|
230
|
+
# Find the longest summary
|
231
|
+
best_summary = summaries.max_by(&:length)
|
232
|
+
markdown << "> #{best_summary}\n\n"
|
233
|
+
end
|
234
|
+
|
235
|
+
# Add repositories
|
236
|
+
if repositories.any?
|
237
|
+
markdown << "**Repositories:**\n\n"
|
238
|
+
repositories.each do |repo|
|
239
|
+
if repo[:commits] > 0 || repo[:changes] > 0
|
240
|
+
markdown << "- #{repo[:name]} (#{repo[:org]}): #{repo[:commits]} commits, #{repo[:changes]} lines changed\n"
|
241
|
+
else
|
242
|
+
markdown << "- #{repo[:name]} (#{repo[:org]})\n"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
markdown << "\n"
|
246
|
+
end
|
247
|
+
|
248
|
+
# Add languages if available (combining from all orgs)
|
249
|
+
all_languages = {}
|
250
|
+
user[:organizations].each do |org_name|
|
251
|
+
org_data = user[:org_details][org_name]
|
252
|
+
next unless org_data
|
253
|
+
|
254
|
+
if org_data[:data][:language_distribution].is_a?(Hash) && !org_data[:data][:language_distribution].empty?
|
255
|
+
org_data[:data][:language_distribution].each do |lang, percentage|
|
256
|
+
all_languages[lang] ||= 0
|
257
|
+
all_languages[lang] += percentage.to_f
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
if all_languages.any?
|
263
|
+
# Normalize percentages
|
264
|
+
total = all_languages.values.sum
|
265
|
+
all_languages.each { |lang, value| all_languages[lang] = (value / total * 100) }
|
266
|
+
|
267
|
+
# Use the new function to generate visual language distribution
|
268
|
+
markdown << generate_language_distribution_markdown(all_languages)
|
269
|
+
end
|
270
|
+
|
271
|
+
# Add recent commit messages combining from all orgs
|
272
|
+
all_commits = []
|
273
|
+
user[:organizations].each do |org_name|
|
274
|
+
org_data = user[:org_details][org_name]
|
275
|
+
next unless org_data
|
276
|
+
|
277
|
+
if org_data[:data]["recent_commits"].is_a?(Array) && !org_data[:data]["recent_commits"].empty?
|
278
|
+
org_data[:data]["recent_commits"].each do |commit|
|
279
|
+
message = commit["message"] || commit[:message]
|
280
|
+
repo = commit["repository"] || commit[:repository]
|
281
|
+
all_commits << {
|
282
|
+
message: message,
|
283
|
+
repo: repo,
|
284
|
+
org: org_name
|
285
|
+
}
|
286
|
+
end
|
287
|
+
elsif org_data[:data]["commits"].is_a?(Array) && !org_data[:data]["commits"].empty?
|
288
|
+
org_data[:data]["commits"].each do |commit|
|
289
|
+
message = commit["message"] || commit[:message] || "No message"
|
290
|
+
repo = commit["repository"] || commit[:repository] || "Unknown repository"
|
291
|
+
all_commits << {
|
292
|
+
message: message,
|
293
|
+
repo: repo,
|
294
|
+
org: org_name
|
295
|
+
}
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
if all_commits.any?
|
301
|
+
markdown << "**Recent Work:**\n\n"
|
302
|
+
all_commits.take(5).each do |commit|
|
303
|
+
markdown << "- #{commit[:repo]} (#{commit[:org]}): #{commit[:message]}\n"
|
304
|
+
end
|
305
|
+
markdown << "\n"
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
return markdown
|
311
|
+
end
|
312
|
+
|
313
|
+
# Generate a visual representation of language distribution for markdown
|
314
|
+
def generate_language_distribution_markdown(languages)
|
315
|
+
return "" if languages.nil? || !languages.is_a?(Hash) || languages.empty?
|
316
|
+
|
317
|
+
markdown = "**Languages Used:**\n\n"
|
318
|
+
|
319
|
+
# Sort languages by percentage (highest first)
|
320
|
+
sorted_languages = languages.sort_by { |_, v| -v }
|
321
|
+
|
322
|
+
# Calculate the max bar width (in characters)
|
323
|
+
max_bar_width = 30
|
324
|
+
|
325
|
+
sorted_languages.each do |lang, percentage|
|
326
|
+
# Calculate bar width based on percentage
|
327
|
+
bar_width = (percentage * max_bar_width / 100.0).round
|
328
|
+
bar = "█" * bar_width
|
329
|
+
|
330
|
+
# Format the percentage to one decimal place
|
331
|
+
formatted_percentage = percentage.round(1)
|
332
|
+
|
333
|
+
# Add the language bar
|
334
|
+
markdown << "- **#{lang}**: #{formatted_percentage}% #{bar}\n"
|
335
|
+
end
|
336
|
+
|
337
|
+
markdown << "\n"
|
338
|
+
return markdown
|
339
|
+
end
|
340
|
+
|
341
|
+
def self.generate_active_users_table(user_activity, org_activity)
|
342
|
+
return "No active users found in this time period." if user_activity.nil? || user_activity.empty?
|
343
|
+
|
344
|
+
# Calculate total contribution score for each user based on weights
|
345
|
+
user_activity.each do |user|
|
346
|
+
analysis = user[:gemini_analysis] || {}
|
347
|
+
|
348
|
+
# Get weights from either string or symbol keys
|
349
|
+
weights = nil
|
350
|
+
if analysis["contribution_weights"].is_a?(Hash)
|
351
|
+
weights = analysis["contribution_weights"]
|
352
|
+
elsif analysis[:contribution_weights].is_a?(Hash)
|
353
|
+
weights = analysis[:contribution_weights]
|
354
|
+
else
|
355
|
+
weights = {
|
356
|
+
"lines_of_code" => 0,
|
357
|
+
"complexity" => 0,
|
358
|
+
"technical_depth" => 0,
|
359
|
+
"scope" => 0,
|
360
|
+
"pr_reviews" => 0
|
361
|
+
}
|
362
|
+
end
|
363
|
+
|
364
|
+
# Calculate total score as sum of all weights
|
365
|
+
total_score = 0
|
366
|
+
if weights
|
367
|
+
if weights.is_a?(Hash)
|
368
|
+
total_score += weights["lines_of_code"].to_i rescue 0
|
369
|
+
total_score += weights["complexity"].to_i rescue 0
|
370
|
+
total_score += weights["technical_depth"].to_i rescue 0
|
371
|
+
total_score += weights["scope"].to_i rescue 0
|
372
|
+
total_score += weights["pr_reviews"].to_i rescue 0
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
# Use the total_score from analysis if it exists, otherwise use calculated score
|
377
|
+
if analysis["total_score"].to_i > 0
|
378
|
+
total_score = analysis["total_score"].to_i
|
379
|
+
elsif analysis[:total_score].to_i > 0
|
380
|
+
total_score = analysis[:total_score].to_i
|
381
|
+
end
|
382
|
+
|
383
|
+
# Store the total score for sorting
|
384
|
+
user[:total_contribution_score] = total_score
|
385
|
+
end
|
386
|
+
|
387
|
+
# Sort users by total contribution score (highest to lowest)
|
388
|
+
sorted_users = user_activity.sort_by { |user| -user[:total_contribution_score].to_i }
|
389
|
+
|
390
|
+
user_rows = []
|
391
|
+
|
392
|
+
# Create rows for each user
|
393
|
+
sorted_users.each do |user|
|
394
|
+
# Use the gemini analysis if available, otherwise use fallback values
|
395
|
+
analysis = user[:gemini_analysis] || {}
|
396
|
+
commits = user[:commit_count] || 0
|
397
|
+
|
398
|
+
# Get projects from either string or symbol key
|
399
|
+
projects = analysis["projects"] || analysis[:projects] || []
|
400
|
+
# Display projects as a comma-separated list, or "N/A" if none
|
401
|
+
project_list = projects.empty? ? "N/A" : (projects.is_a?(Array) ? projects.join(", ") : projects.to_s)
|
402
|
+
|
403
|
+
# Get weights from either string or symbol keys
|
404
|
+
weights = analysis["contribution_weights"] || analysis[:contribution_weights] || {}
|
405
|
+
|
406
|
+
# Format the weights as a visual indicator
|
407
|
+
loc_weight = weights["lines_of_code"].to_i rescue 0
|
408
|
+
complexity_weight = weights["complexity"].to_i rescue 0
|
409
|
+
depth_weight = weights["technical_depth"].to_i rescue 0
|
410
|
+
scope_weight = weights["scope"].to_i rescue 0
|
411
|
+
pr_weight = weights["pr_reviews"].to_i rescue 0
|
412
|
+
|
413
|
+
total_score = user[:total_contribution_score].to_i
|
414
|
+
|
415
|
+
# Format contribution score with visual indicator
|
416
|
+
score_display = case total_score
|
417
|
+
when 30..50
|
418
|
+
"🔥 #{total_score}" # High contribution
|
419
|
+
when 15..29
|
420
|
+
"👍 #{total_score}" # Medium contribution
|
421
|
+
else
|
422
|
+
"#{total_score}" # Low contribution
|
423
|
+
end
|
424
|
+
|
425
|
+
# Format the weights table
|
426
|
+
weights_display = "LOC: #{loc_weight} | Complexity: #{complexity_weight} | Depth: #{depth_weight} | Scope: #{scope_weight} | PR: #{pr_weight}"
|
427
|
+
|
428
|
+
# Get other fields from either string or symbol keys
|
429
|
+
pr_count = analysis["pr_count"] || analysis[:pr_count] || 0
|
430
|
+
lines_changed = analysis["lines_changed"] || analysis[:lines_changed] || 0
|
431
|
+
summary = analysis["summary"] || analysis[:summary] || "N/A"
|
432
|
+
|
433
|
+
# Create row with user info and stats
|
434
|
+
user_rows << "| #{user[:username]} | #{commits} | #{pr_count} | #{lines_changed} | #{score_display} | #{weights_display} | #{summary} | #{project_list} |"
|
435
|
+
end
|
436
|
+
|
437
|
+
# Create the table with headers and all rows
|
438
|
+
<<~MARKDOWN
|
439
|
+
## Active Users
|
440
|
+
|
441
|
+
Users are sorted by their total contribution score, which is calculated as the sum of individual contribution weights.
|
442
|
+
Each contribution weight is on a scale of 0-10 and considers different aspects of contribution value.
|
443
|
+
|
444
|
+
| User | Commits | PRs | Lines Changed | Total Score | Contribution Weights | Summary | Projects |
|
445
|
+
|------|---------|-----|---------------|-------------|----------------------|---------|----------|
|
446
|
+
#{user_rows.join("\n")}
|
447
|
+
|
448
|
+
MARKDOWN
|
449
|
+
end
|
450
|
+
|
451
|
+
def self.generate_combined_user_table(combined_users, users_with_gemini_activity, inactive_users, commits_by_user)
|
452
|
+
return "No active users found in this time period." if combined_users.nil? || combined_users.empty?
|
453
|
+
|
454
|
+
# Calculate total contribution score for each user based on weights
|
455
|
+
combined_users.each do |username, user_data|
|
456
|
+
next if username == :_meta
|
457
|
+
|
458
|
+
# Get weights from various potential sources
|
459
|
+
weights = nil
|
460
|
+
if users_with_gemini_activity[username] && users_with_gemini_activity[username][:contribution_weights]
|
461
|
+
weights = users_with_gemini_activity[username][:contribution_weights]
|
462
|
+
elsif user_data[:contribution_weights]
|
463
|
+
weights = user_data[:contribution_weights]
|
464
|
+
else
|
465
|
+
# Create default weights based on activity
|
466
|
+
commits = user_data[:commits].to_i || 0
|
467
|
+
pr_reviews = user_data[:pr_reviews].to_i || 0
|
468
|
+
lines_changed = user_data[:lines_changed].to_i || 0
|
469
|
+
project_count = user_data[:projects].size || 0
|
470
|
+
|
471
|
+
# Create appropriate weights based on activity metrics
|
472
|
+
loc_weight = if lines_changed > 20000
|
473
|
+
8
|
474
|
+
elsif lines_changed > 10000
|
475
|
+
6
|
476
|
+
elsif lines_changed > 5000
|
477
|
+
4
|
478
|
+
elsif lines_changed > 1000
|
479
|
+
2
|
480
|
+
else
|
481
|
+
1
|
482
|
+
end
|
483
|
+
|
484
|
+
scope_weight = if project_count > 4
|
485
|
+
8
|
486
|
+
elsif project_count > 2
|
487
|
+
5
|
488
|
+
elsif project_count > 0
|
489
|
+
3
|
490
|
+
else
|
491
|
+
1
|
492
|
+
end
|
493
|
+
|
494
|
+
commit_weight = if commits > 50
|
495
|
+
8
|
496
|
+
elsif commits > 20
|
497
|
+
6
|
498
|
+
elsif commits > 10
|
499
|
+
4
|
500
|
+
else
|
501
|
+
1
|
502
|
+
end
|
503
|
+
|
504
|
+
pr_weight = if pr_reviews > 20
|
505
|
+
8
|
506
|
+
elsif pr_reviews > 10
|
507
|
+
6
|
508
|
+
elsif pr_reviews > 5
|
509
|
+
4
|
510
|
+
else
|
511
|
+
1
|
512
|
+
end
|
513
|
+
|
514
|
+
# Provide reasonable default weights
|
515
|
+
weights = {
|
516
|
+
"lines_of_code" => loc_weight,
|
517
|
+
"complexity" => [commit_weight, 3].min,
|
518
|
+
"technical_depth" => 3,
|
519
|
+
"scope" => scope_weight,
|
520
|
+
"pr_reviews" => pr_weight
|
521
|
+
}
|
522
|
+
end
|
523
|
+
|
524
|
+
# Calculate total score as sum of weights
|
525
|
+
total_score = 0
|
526
|
+
if weights
|
527
|
+
if weights.is_a?(Hash)
|
528
|
+
total_score += weights["lines_of_code"].to_i rescue 0
|
529
|
+
total_score += weights["complexity"].to_i rescue 0
|
530
|
+
total_score += weights["technical_depth"].to_i rescue 0
|
531
|
+
total_score += weights["scope"].to_i rescue 0
|
532
|
+
total_score += weights["pr_reviews"].to_i rescue 0
|
533
|
+
end
|
534
|
+
end
|
535
|
+
|
536
|
+
# Store the weights and total score
|
537
|
+
user_data[:contribution_weights] = weights
|
538
|
+
user_data[:total_contribution_score] = total_score
|
539
|
+
end
|
540
|
+
|
541
|
+
# Sort users by contribution score (highest to lowest)
|
542
|
+
sorted_users = combined_users.keys.reject { |username| username == :_meta }.sort_by do |username|
|
543
|
+
# Return a tuple for sorting: active users first, then by total score
|
544
|
+
# Negative values ensure descending order
|
545
|
+
user_data = combined_users[username]
|
546
|
+
has_activity = users_with_gemini_activity.key?(username) || commits_by_user.key?(username)
|
547
|
+
total_score = user_data[:total_contribution_score] || 0
|
548
|
+
|
549
|
+
[-1 * (has_activity ? 1 : 0), -1 * total_score]
|
550
|
+
end
|
551
|
+
|
552
|
+
user_rows = []
|
553
|
+
|
554
|
+
# Create rows for each user
|
555
|
+
sorted_users.each do |username|
|
556
|
+
user_data = combined_users[username]
|
557
|
+
|
558
|
+
# Format project names in a more readable way
|
559
|
+
projects = user_data[:projects].to_a if user_data[:projects]
|
560
|
+
project_names = if projects&.any?
|
561
|
+
projects.map { |p| p.split('/').last }.join(', ')
|
562
|
+
else
|
563
|
+
"-"
|
564
|
+
end
|
565
|
+
|
566
|
+
# Format the output fields with defaults
|
567
|
+
commits = user_data[:commits] || 0
|
568
|
+
pr_reviews = user_data[:pr_reviews] || 0
|
569
|
+
lines_changed = user_data[:lines_changed] || 0
|
570
|
+
total_score = user_data[:total_contribution_score] || 0
|
571
|
+
|
572
|
+
# Format contribution score with visual indicator
|
573
|
+
score_display = case total_score
|
574
|
+
when 30..50
|
575
|
+
"🔥 #{total_score}" # High contribution
|
576
|
+
when 15..29
|
577
|
+
"👍 #{total_score}" # Medium contribution
|
578
|
+
else
|
579
|
+
"#{total_score}" # Low contribution
|
580
|
+
end
|
581
|
+
|
582
|
+
# Format the weights
|
583
|
+
weights = user_data[:contribution_weights] || {}
|
584
|
+
loc_weight = weights["lines_of_code"].to_i || weights[:lines_of_code].to_i || 0
|
585
|
+
complexity_weight = weights["complexity"].to_i || weights[:complexity].to_i || 0
|
586
|
+
depth_weight = weights["technical_depth"].to_i || weights[:technical_depth].to_i || 0
|
587
|
+
scope_weight = weights["scope"].to_i || weights[:scope].to_i || 0
|
588
|
+
pr_weight = weights["pr_reviews"].to_i || weights[:pr_reviews].to_i || 0
|
589
|
+
|
590
|
+
weights_display = "LOC: #{loc_weight} | Complexity: #{complexity_weight} | Depth: #{depth_weight} | Scope: #{scope_weight} | PR: #{pr_weight}"
|
591
|
+
|
592
|
+
user_rows << "| **#{username}** | #{commits} | #{pr_reviews} | #{lines_changed} | #{score_display} | #{weights_display} | #{project_names} |"
|
593
|
+
end
|
594
|
+
|
595
|
+
# Create the table with headers and all rows
|
596
|
+
<<~MARKDOWN
|
597
|
+
## Active Users
|
598
|
+
|
599
|
+
Users are sorted by their total contribution score, which is calculated as the sum of individual contribution weights.
|
600
|
+
Each contribution weight is on a scale of 0-10 and considers different aspects of contribution value.
|
601
|
+
|
602
|
+
| User | Commits | PR Reviews | Lines Changed | Total Score | Contribution Weights | Projects |
|
603
|
+
|------|---------|------------|---------------|-------------|----------------------|----------|
|
604
|
+
#{user_rows.join("\n")}
|
605
|
+
|
606
|
+
MARKDOWN
|
607
|
+
end
|
608
|
+
|
609
|
+
# Process data from all organizations and extract user activity information
|
610
|
+
def self.process_data(results, logger = nil)
|
611
|
+
logger ||= Logger.new($stdout)
|
612
|
+
|
613
|
+
# Combined user activity across all organizations
|
614
|
+
combined_users = {}
|
615
|
+
|
616
|
+
# Users with Gemini-analyzed activity data
|
617
|
+
users_with_gemini_activity = {}
|
618
|
+
|
619
|
+
# Collect all users with commit data
|
620
|
+
commits_by_user = {}
|
621
|
+
|
622
|
+
# Skip specific keys that aren't organizations
|
623
|
+
skip_keys = ["_meta", :_meta, "summary_statistics", :summary_statistics]
|
624
|
+
|
625
|
+
# Get all organizations from the results
|
626
|
+
organizations = results.keys.select do |k|
|
627
|
+
!skip_keys.include?(k)
|
628
|
+
end
|
629
|
+
|
630
|
+
# Collect all users across all organizations
|
631
|
+
organizations.each do |org_name|
|
632
|
+
next if skip_keys.include?(org_name)
|
633
|
+
|
634
|
+
# Skip if this is not an organization data hash
|
635
|
+
next unless results[org_name].is_a?(Hash)
|
636
|
+
|
637
|
+
if results[org_name].key?("users")
|
638
|
+
org_users = results[org_name]["users"]
|
639
|
+
logger.info("Processing organization #{org_name} with #{org_users.keys.size} users")
|
640
|
+
|
641
|
+
org_users.each do |username, user_data|
|
642
|
+
next if username == :_meta || username == "_meta" # Skip metadata entries
|
643
|
+
next unless user_data.is_a?(Hash)
|
644
|
+
|
645
|
+
combined_users[username] ||= {
|
646
|
+
commits: 0,
|
647
|
+
pr_reviews: 0,
|
648
|
+
projects: Set.new,
|
649
|
+
organizations: Set.new,
|
650
|
+
contribution_score: 0,
|
651
|
+
lines_changed: 0,
|
652
|
+
}
|
653
|
+
|
654
|
+
# Add organization name
|
655
|
+
combined_users[username][:organizations].add(org_name)
|
656
|
+
|
657
|
+
# Add commit count - ensure it's an integer
|
658
|
+
commits = user_data[:changes].to_i rescue 0
|
659
|
+
commits = user_data["changes"].to_i if commits == 0 && user_data["changes"]
|
660
|
+
|
661
|
+
combined_users[username][:commits] += commits
|
662
|
+
|
663
|
+
# Track commits by user
|
664
|
+
if commits > 0
|
665
|
+
commits_by_user[username] ||= 0
|
666
|
+
commits_by_user[username] += commits
|
667
|
+
end
|
668
|
+
|
669
|
+
# Extract PR count and add it
|
670
|
+
pr_count = user_data[:pr_count].to_i rescue 0
|
671
|
+
pr_count = user_data["pr_count"].to_i if pr_count == 0 && user_data["pr_count"]
|
672
|
+
|
673
|
+
combined_users[username][:pr_reviews] += pr_count
|
674
|
+
|
675
|
+
# Extract total score if available
|
676
|
+
if user_data[:total_score].to_i > 0
|
677
|
+
combined_users[username][:contribution_score] = [
|
678
|
+
combined_users[username][:contribution_score],
|
679
|
+
user_data[:total_score].to_i
|
680
|
+
].max
|
681
|
+
elsif user_data["total_score"].to_i > 0
|
682
|
+
combined_users[username][:contribution_score] = [
|
683
|
+
combined_users[username][:contribution_score],
|
684
|
+
user_data["total_score"].to_i
|
685
|
+
].max
|
686
|
+
end
|
687
|
+
|
688
|
+
# Extract lines changed
|
689
|
+
lines_changed = user_data[:lines_changed].to_i rescue 0
|
690
|
+
lines_changed = user_data["lines_changed"].to_i if lines_changed == 0 && user_data["lines_changed"]
|
691
|
+
|
692
|
+
combined_users[username][:lines_changed] += lines_changed
|
693
|
+
|
694
|
+
# Extract projects data
|
695
|
+
if user_data[:projects] || user_data["projects"]
|
696
|
+
extracted_projects = user_data[:projects] || user_data["projects"] || []
|
697
|
+
if extracted_projects.is_a?(Array)
|
698
|
+
extracted_projects.each do |project|
|
699
|
+
combined_users[username][:projects].add(project) if project && !project.empty?
|
700
|
+
end
|
701
|
+
elsif extracted_projects.is_a?(String)
|
702
|
+
combined_users[username][:projects].add(extracted_projects) if !extracted_projects.empty?
|
703
|
+
end
|
704
|
+
end
|
705
|
+
|
706
|
+
# Check for Gemini activity data
|
707
|
+
has_activity = commits > 0 ||
|
708
|
+
combined_users[username][:contribution_score] > 0 ||
|
709
|
+
lines_changed > 0 ||
|
710
|
+
pr_count > 0
|
711
|
+
|
712
|
+
# Save Gemini analysis data if it exists
|
713
|
+
if has_activity
|
714
|
+
logger.info("User #{username} has Gemini activity data")
|
715
|
+
|
716
|
+
users_with_gemini_activity[username] ||= {
|
717
|
+
changes: 0,
|
718
|
+
contribution_score: 0,
|
719
|
+
lines_changed: 0,
|
720
|
+
pr_count: 0,
|
721
|
+
projects: [],
|
722
|
+
summary: "",
|
723
|
+
org_name: org_name,
|
724
|
+
contribution_weights: {
|
725
|
+
"lines_of_code" => 0,
|
726
|
+
"complexity" => 0,
|
727
|
+
"technical_depth" => 0,
|
728
|
+
"scope" => 0,
|
729
|
+
"pr_reviews" => 0
|
730
|
+
}
|
731
|
+
}
|
732
|
+
|
733
|
+
# Update with this organization's data
|
734
|
+
users_with_gemini_activity[username][:changes] += commits if commits > 0
|
735
|
+
users_with_gemini_activity[username][:pr_count] += pr_count if pr_count > 0
|
736
|
+
|
737
|
+
# Extract total score if available
|
738
|
+
if user_data[:total_score].to_i > 0
|
739
|
+
users_with_gemini_activity[username][:contribution_score] = [
|
740
|
+
users_with_gemini_activity[username][:contribution_score],
|
741
|
+
user_data[:total_score].to_i
|
742
|
+
].max
|
743
|
+
elsif user_data["total_score"].to_i > 0
|
744
|
+
users_with_gemini_activity[username][:contribution_score] = [
|
745
|
+
users_with_gemini_activity[username][:contribution_score],
|
746
|
+
user_data["total_score"].to_i
|
747
|
+
].max
|
748
|
+
end
|
749
|
+
|
750
|
+
users_with_gemini_activity[username][:lines_changed] += lines_changed if lines_changed > 0
|
751
|
+
|
752
|
+
# Extract contribution weights if they exist
|
753
|
+
if user_data[:contribution_weights].is_a?(Hash) || user_data["contribution_weights"].is_a?(Hash)
|
754
|
+
weights = user_data[:contribution_weights] || user_data["contribution_weights"]
|
755
|
+
|
756
|
+
if weights.is_a?(Hash)
|
757
|
+
# Copy each weight, using the highest value if it already exists
|
758
|
+
["lines_of_code", "complexity", "technical_depth", "scope", "pr_reviews"].each do |key|
|
759
|
+
# Try string or symbol key in the source weights
|
760
|
+
weight_value = weights[key].to_i rescue weights[key.to_sym].to_i rescue 0
|
761
|
+
|
762
|
+
# Update if the new value is higher
|
763
|
+
current_value = users_with_gemini_activity[username][:contribution_weights][key].to_i
|
764
|
+
if weight_value > current_value
|
765
|
+
users_with_gemini_activity[username][:contribution_weights][key] = weight_value
|
766
|
+
end
|
767
|
+
end
|
768
|
+
|
769
|
+
logger.debug("Updated contribution_weights for #{username}: #{users_with_gemini_activity[username][:contribution_weights].inspect}")
|
770
|
+
end
|
771
|
+
end
|
772
|
+
|
773
|
+
# Calculate total score if not already set
|
774
|
+
if users_with_gemini_activity[username][:contribution_score] == 0
|
775
|
+
total = 0
|
776
|
+
users_with_gemini_activity[username][:contribution_weights].each do |key, value|
|
777
|
+
total += value.to_i
|
778
|
+
end
|
779
|
+
users_with_gemini_activity[username][:contribution_score] = total
|
780
|
+
logger.debug("Calculated total score for #{username}: #{total}")
|
781
|
+
end
|
782
|
+
|
783
|
+
# Extract summary
|
784
|
+
if user_data[:summary] || user_data["summary"]
|
785
|
+
summary = user_data[:summary] || user_data["summary"]
|
786
|
+
if summary && !summary.empty? && summary != "No activity detected in the specified time window."
|
787
|
+
users_with_gemini_activity[username][:summary] = summary
|
788
|
+
end
|
789
|
+
end
|
790
|
+
|
791
|
+
# Add projects
|
792
|
+
if user_data[:projects] || user_data["projects"]
|
793
|
+
extracted_projects = user_data[:projects] || user_data["projects"] || []
|
794
|
+
if extracted_projects.is_a?(Array)
|
795
|
+
users_with_gemini_activity[username][:projects] += extracted_projects
|
796
|
+
elsif extracted_projects.is_a?(String) && !extracted_projects.empty?
|
797
|
+
users_with_gemini_activity[username][:projects] << extracted_projects
|
798
|
+
end
|
799
|
+
|
800
|
+
# Ensure uniqueness of projects
|
801
|
+
users_with_gemini_activity[username][:projects].uniq!
|
802
|
+
end
|
803
|
+
end
|
804
|
+
end
|
805
|
+
end
|
806
|
+
end
|
807
|
+
|
808
|
+
# Get all active users (those with any activity)
|
809
|
+
active_users = combined_users.keys.reject { |username| username == :_meta }
|
810
|
+
|
811
|
+
# Get inactive users (those without activity in active_users)
|
812
|
+
inactive_users = combined_users.keys.reject { |username| username == :_meta }
|
813
|
+
|
814
|
+
# Return all processed data
|
815
|
+
[combined_users, users_with_gemini_activity, inactive_users, commits_by_user]
|
816
|
+
end
|
817
|
+
end
|
818
|
+
end
|