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,163 @@
|
|
1
|
+
module GithubDailyDigest
|
2
|
+
class LanguageAnalyzer
|
3
|
+
# Map of file extensions to languages
|
4
|
+
EXTENSION_TO_LANGUAGE = {
|
5
|
+
# Ruby
|
6
|
+
'.rb' => 'Ruby',
|
7
|
+
'.rake' => 'Ruby',
|
8
|
+
'.gemspec' => 'Ruby',
|
9
|
+
|
10
|
+
# JavaScript
|
11
|
+
'.js' => 'JavaScript',
|
12
|
+
'.jsx' => 'JavaScript',
|
13
|
+
'.mjs' => 'JavaScript',
|
14
|
+
'.cjs' => 'JavaScript',
|
15
|
+
'.ts' => 'TypeScript',
|
16
|
+
'.tsx' => 'TypeScript',
|
17
|
+
|
18
|
+
# HTML/CSS
|
19
|
+
'.html' => 'HTML',
|
20
|
+
'.htm' => 'HTML',
|
21
|
+
'.xhtml' => 'HTML',
|
22
|
+
'.erb' => 'HTML/ERB',
|
23
|
+
'.haml' => 'HTML/Haml',
|
24
|
+
'.slim' => 'HTML/Slim',
|
25
|
+
'.css' => 'CSS',
|
26
|
+
'.scss' => 'CSS/SCSS',
|
27
|
+
'.sass' => 'CSS/SASS',
|
28
|
+
'.less' => 'CSS/LESS',
|
29
|
+
|
30
|
+
# PHP
|
31
|
+
'.php' => 'PHP',
|
32
|
+
'.phtml' => 'PHP',
|
33
|
+
|
34
|
+
# Python
|
35
|
+
'.py' => 'Python',
|
36
|
+
'.pyd' => 'Python',
|
37
|
+
'.pyo' => 'Python',
|
38
|
+
'.pyw' => 'Python',
|
39
|
+
|
40
|
+
# Java
|
41
|
+
'.java' => 'Java',
|
42
|
+
'.class' => 'Java',
|
43
|
+
'.jar' => 'Java',
|
44
|
+
|
45
|
+
# C/C++
|
46
|
+
'.c' => 'C',
|
47
|
+
'.h' => 'C',
|
48
|
+
'.cpp' => 'C++',
|
49
|
+
'.cc' => 'C++',
|
50
|
+
'.cxx' => 'C++',
|
51
|
+
'.hpp' => 'C++',
|
52
|
+
|
53
|
+
# C#
|
54
|
+
'.cs' => 'C#',
|
55
|
+
|
56
|
+
# Go
|
57
|
+
'.go' => 'Go',
|
58
|
+
|
59
|
+
# Swift
|
60
|
+
'.swift' => 'Swift',
|
61
|
+
|
62
|
+
# Kotlin
|
63
|
+
'.kt' => 'Kotlin',
|
64
|
+
'.kts' => 'Kotlin',
|
65
|
+
|
66
|
+
# Rust
|
67
|
+
'.rs' => 'Rust',
|
68
|
+
|
69
|
+
# Shell
|
70
|
+
'.sh' => 'Shell',
|
71
|
+
'.bash' => 'Shell',
|
72
|
+
'.zsh' => 'Shell',
|
73
|
+
'.fish' => 'Shell',
|
74
|
+
|
75
|
+
# Data
|
76
|
+
'.json' => 'JSON',
|
77
|
+
'.xml' => 'XML',
|
78
|
+
'.yaml' => 'YAML',
|
79
|
+
'.yml' => 'YAML',
|
80
|
+
'.csv' => 'CSV',
|
81
|
+
'.toml' => 'TOML',
|
82
|
+
|
83
|
+
# Config
|
84
|
+
'.ini' => 'Config',
|
85
|
+
'.conf' => 'Config',
|
86
|
+
'.cfg' => 'Config',
|
87
|
+
|
88
|
+
# Markdown
|
89
|
+
'.md' => 'Markdown',
|
90
|
+
'.markdown' => 'Markdown',
|
91
|
+
|
92
|
+
# SQL
|
93
|
+
'.sql' => 'SQL',
|
94
|
+
|
95
|
+
# Other
|
96
|
+
'.txt' => 'Text',
|
97
|
+
'.gitignore' => 'Git',
|
98
|
+
'.dockerignore' => 'Docker',
|
99
|
+
'Dockerfile' => 'Docker',
|
100
|
+
'.env' => 'Config'
|
101
|
+
}.freeze
|
102
|
+
|
103
|
+
# Identify language from filename
|
104
|
+
def self.identify_language(filepath)
|
105
|
+
# Get the file extension
|
106
|
+
ext = File.extname(filepath).downcase
|
107
|
+
|
108
|
+
# For files without extensions, check the full filename
|
109
|
+
basename = File.basename(filepath)
|
110
|
+
|
111
|
+
# Try to match by extension first
|
112
|
+
return EXTENSION_TO_LANGUAGE[ext] if EXTENSION_TO_LANGUAGE.key?(ext)
|
113
|
+
|
114
|
+
# Try to match by filename for special cases
|
115
|
+
return EXTENSION_TO_LANGUAGE[basename] if EXTENSION_TO_LANGUAGE.key?(basename)
|
116
|
+
|
117
|
+
# Default for unknown types
|
118
|
+
'Other'
|
119
|
+
end
|
120
|
+
|
121
|
+
# Calculate language distribution from a list of files
|
122
|
+
# files should be an array of hashes with at least :path, :additions, :deletions
|
123
|
+
def self.calculate_distribution(files)
|
124
|
+
return {} if files.nil? || files.empty?
|
125
|
+
|
126
|
+
# Initialize counters
|
127
|
+
language_stats = Hash.new(0)
|
128
|
+
total_changes = 0
|
129
|
+
|
130
|
+
files.each do |file|
|
131
|
+
# Skip files with no path
|
132
|
+
next unless file[:path]
|
133
|
+
|
134
|
+
# Calculate the total lines changed for this file
|
135
|
+
lines_changed = (file[:additions] || 0) + (file[:deletions] || 0)
|
136
|
+
|
137
|
+
# Identify the language
|
138
|
+
language = identify_language(file[:path])
|
139
|
+
|
140
|
+
# Add to the counters
|
141
|
+
language_stats[language] += lines_changed
|
142
|
+
total_changes += lines_changed
|
143
|
+
end
|
144
|
+
|
145
|
+
# Convert to percentages
|
146
|
+
result = {}
|
147
|
+
|
148
|
+
language_stats.each do |language, count|
|
149
|
+
# Skip languages with 0 changes (shouldn't happen, but just in case)
|
150
|
+
next if count == 0
|
151
|
+
|
152
|
+
# Calculate percentage (rounded to 1 decimal place)
|
153
|
+
percentage = total_changes > 0 ? (count.to_f / total_changes * 100).round(1) : 0
|
154
|
+
|
155
|
+
# Only include if it's more than 0.1%
|
156
|
+
result[language] = percentage if percentage > 0.1
|
157
|
+
end
|
158
|
+
|
159
|
+
# Sort by percentage (descending)
|
160
|
+
result.sort_by { |_, percentage| -percentage }.to_h
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# A temporary solution file to generate proper markdown output with contribution weights
|
4
|
+
# Run this after generating JSON output from github-daily-digest
|
5
|
+
|
6
|
+
require 'json'
|
7
|
+
require 'optparse'
|
8
|
+
|
9
|
+
options = {
|
10
|
+
input_file: nil,
|
11
|
+
output_file: nil
|
12
|
+
}
|
13
|
+
|
14
|
+
OptionParser.new do |opts|
|
15
|
+
opts.banner = "Usage: ruby markdown_formatter.rb [options]"
|
16
|
+
|
17
|
+
opts.on("-i", "--input FILE", "Input JSON file") do |file|
|
18
|
+
options[:input_file] = file
|
19
|
+
end
|
20
|
+
|
21
|
+
opts.on("-o", "--output FILE", "Output Markdown file") do |file|
|
22
|
+
options[:output_file] = file
|
23
|
+
end
|
24
|
+
end.parse!
|
25
|
+
|
26
|
+
unless options[:input_file]
|
27
|
+
puts "Error: Input file is required"
|
28
|
+
exit 1
|
29
|
+
end
|
30
|
+
|
31
|
+
# Read the JSON file
|
32
|
+
json_data = JSON.parse(File.read(options[:input_file]))
|
33
|
+
|
34
|
+
# Generate markdown
|
35
|
+
markdown = "# GitHub Activity Digest\n\n"
|
36
|
+
markdown << "Generated on: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
37
|
+
|
38
|
+
# Add overview section
|
39
|
+
markdown << "## Overview\n\n"
|
40
|
+
markdown << "| Category | Value |\n"
|
41
|
+
markdown << "| --- | --- |\n"
|
42
|
+
markdown << "| **Time Period** | Last 7 days |\n"
|
43
|
+
|
44
|
+
# Get organizations
|
45
|
+
organizations = json_data.keys.reject { |k| k == "_meta" }
|
46
|
+
markdown << "| **Organizations** | #{organizations.join(', ')} |\n"
|
47
|
+
markdown << "| **Data Source** | GitHub API |\n\n"
|
48
|
+
|
49
|
+
# Process users
|
50
|
+
all_users = {}
|
51
|
+
organizations.each do |org|
|
52
|
+
json_data[org].each do |username, user_data|
|
53
|
+
next if username == "_meta"
|
54
|
+
|
55
|
+
# Skip users with no activity
|
56
|
+
next if user_data["changes"].to_i == 0 && user_data["pr_count"].to_i == 0
|
57
|
+
|
58
|
+
all_users[username] ||= {
|
59
|
+
username: username,
|
60
|
+
commits: 0,
|
61
|
+
prs: 0,
|
62
|
+
lines_changed: 0,
|
63
|
+
projects: [],
|
64
|
+
total_score: 0,
|
65
|
+
weights: {
|
66
|
+
"lines_of_code" => 0,
|
67
|
+
"complexity" => 0,
|
68
|
+
"technical_depth" => 0,
|
69
|
+
"scope" => 0,
|
70
|
+
"pr_reviews" => 0
|
71
|
+
},
|
72
|
+
summary: user_data["summary"] || ""
|
73
|
+
}
|
74
|
+
|
75
|
+
# Accumulate data
|
76
|
+
all_users[username][:commits] += user_data["changes"].to_i
|
77
|
+
all_users[username][:prs] += user_data["pr_count"].to_i
|
78
|
+
all_users[username][:lines_changed] += user_data["lines_changed"].to_i
|
79
|
+
|
80
|
+
# Get projects
|
81
|
+
if user_data["projects"].is_a?(Array)
|
82
|
+
all_users[username][:projects] += user_data["projects"]
|
83
|
+
end
|
84
|
+
|
85
|
+
# Get contribution weights
|
86
|
+
if user_data["contribution_weights"].is_a?(Hash)
|
87
|
+
weights = user_data["contribution_weights"]
|
88
|
+
|
89
|
+
all_users[username][:weights]["lines_of_code"] = [all_users[username][:weights]["lines_of_code"], weights["lines_of_code"].to_i].max
|
90
|
+
all_users[username][:weights]["complexity"] = [all_users[username][:weights]["complexity"], weights["complexity"].to_i].max
|
91
|
+
all_users[username][:weights]["technical_depth"] = [all_users[username][:weights]["technical_depth"], weights["technical_depth"].to_i].max
|
92
|
+
all_users[username][:weights]["scope"] = [all_users[username][:weights]["scope"], weights["scope"].to_i].max
|
93
|
+
all_users[username][:weights]["pr_reviews"] = [all_users[username][:weights]["pr_reviews"], weights["pr_reviews"].to_i].max
|
94
|
+
end
|
95
|
+
|
96
|
+
# Calculate total score
|
97
|
+
all_users[username][:total_score] = 0
|
98
|
+
all_users[username][:weights].each do |key, value|
|
99
|
+
all_users[username][:total_score] += value.to_i
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Sort users by total score
|
105
|
+
sorted_users = all_users.values.sort_by { |user| -user[:total_score] }
|
106
|
+
|
107
|
+
# Add active users section
|
108
|
+
markdown << "## Active Users\n\n"
|
109
|
+
markdown << "Users are sorted by their total contribution score, which is calculated as the sum of individual contribution weights.\n"
|
110
|
+
markdown << "Each contribution weight is on a scale of 0-10 and considers different aspects of contribution value.\n\n"
|
111
|
+
|
112
|
+
# Create users table
|
113
|
+
markdown << "| User | Commits | PRs | Lines Changed | Total Score | Contribution Weights | Summary | Projects |\n"
|
114
|
+
markdown << "|------|---------|-----|---------------|-------------|----------------------|---------|----------|\n"
|
115
|
+
|
116
|
+
sorted_users.each do |user|
|
117
|
+
# Format weights
|
118
|
+
weights_display = "LOC: #{user[:weights]['lines_of_code']} | " +
|
119
|
+
"Complexity: #{user[:weights]['complexity']} | " +
|
120
|
+
"Depth: #{user[:weights]['technical_depth']} | " +
|
121
|
+
"Scope: #{user[:weights]['scope']} | " +
|
122
|
+
"PR: #{user[:weights]['pr_reviews']}"
|
123
|
+
|
124
|
+
# Format projects
|
125
|
+
projects_display = user[:projects].uniq.join(", ")
|
126
|
+
|
127
|
+
# Create row
|
128
|
+
markdown << "| #{user[:username]} | #{user[:commits]} | #{user[:prs]} | #{user[:lines_changed]} | #{user[:total_score]} | #{weights_display} | #{user[:summary]} | #{projects_display} |\n"
|
129
|
+
end
|
130
|
+
|
131
|
+
# Output markdown
|
132
|
+
if options[:output_file]
|
133
|
+
File.write(options[:output_file], markdown)
|
134
|
+
puts "Markdown written to #{options[:output_file]}"
|
135
|
+
else
|
136
|
+
puts markdown
|
137
|
+
end
|