github_contribs 1.1.0 → 2.0.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
- checksums.yaml.gz.sig +0 -0
- data/History.rdoc +14 -0
- data/Rakefile +2 -4
- data/bin/github_contribs +1 -1
- data/lib/github_contribs.rb +223 -48
- data/test/test_github_contribs.rb +3 -8
- data.tar.gz.sig +0 -0
- metadata +13 -27
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 77816e7a617476af27562ba5ec816996cf8a965780c8c317d8d44fc48f6e4758
|
4
|
+
data.tar.gz: 3b448123b525319b886f64b0b7f12f9945b2b4595f57634d53b56e8ba724a8a1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aade65668beea1d2ab92abc531c2e45d292b2fcd23a597669501ed2d9f7ba52b372a8d570b26a41da8aed58b394bf6c5c2771d1797ef70c2623268ffc80e5250
|
7
|
+
data.tar.gz: 2dac0658a3f8dac10067e2ec00b7a8d4144167b55a48c2b9458082d5fd6f4678a18fe903b154316801fd7813187c9727c2fddad231ecb27483b0441943e2fac9
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/History.rdoc
CHANGED
@@ -1,3 +1,17 @@
|
|
1
|
+
=== 2.0.0 / 2024-07-01
|
2
|
+
|
3
|
+
* 2 major enhancements:
|
4
|
+
|
5
|
+
* Completely rewrote data gathering to use graphql. Caches json per year.
|
6
|
+
* Completely rewrote html generation to be from scratch. Much cleaner.
|
7
|
+
|
8
|
+
* 4 minor enhancements:
|
9
|
+
|
10
|
+
* Built a legend and non-intrusive popups to provide better UI.
|
11
|
+
* Dropped nokogiri as a dependency. Now using gh cmdline instead of scraping. This might change.
|
12
|
+
* No longer defaults to the last 10 years. Now does *all* years if start is not specified.
|
13
|
+
* Scale all contributions against min/max to make it easier to compare.
|
14
|
+
|
1
15
|
=== 1.1.0 / 2022-10-05
|
2
16
|
|
3
17
|
* 1 minor enhancement:
|
data/Rakefile
CHANGED
@@ -10,16 +10,14 @@ Hoe.plugin :rdoc
|
|
10
10
|
Hoe.spec "github_contribs" do
|
11
11
|
developer "Ryan Davis", "ryand-ruby@zenspider.com"
|
12
12
|
|
13
|
-
dependency "nokogiri", "~> 1.12"
|
14
|
-
|
15
13
|
self.isolate_multiruby = true # for nokogiri
|
16
14
|
|
17
15
|
license "MIT"
|
18
16
|
end
|
19
17
|
|
20
18
|
task :run => :isolate do
|
21
|
-
WHO = ENV["U"] || "zenspider
|
22
|
-
ruby "-Ilib bin/github_contribs #{WHO}"
|
19
|
+
WHO = ENV["U"] || "zenspider"
|
20
|
+
ruby "-Ilib bin/github_contribs -v #{WHO}"
|
23
21
|
end
|
24
22
|
|
25
23
|
# vim: syntax=ruby
|
data/bin/github_contribs
CHANGED
data/lib/github_contribs.rb
CHANGED
@@ -1,77 +1,252 @@
|
|
1
|
-
require "pp"
|
2
|
-
require "nokogiri"
|
3
1
|
require "fileutils"
|
4
|
-
require "
|
5
|
-
require "
|
2
|
+
require "json"
|
3
|
+
require "date"
|
6
4
|
|
7
5
|
class GithubContribs
|
8
|
-
VERSION = "
|
6
|
+
VERSION = "2.0.0"
|
9
7
|
|
10
|
-
def
|
11
|
-
|
12
|
-
data = YAML.load_file File.expand_path "~/.config/gh/hosts.yml"
|
13
|
-
data && data.dig("github.com", "oauth_token")
|
14
|
-
end
|
8
|
+
def graphql(*args)
|
9
|
+
IO.popen(["gh", "api", "graphql", *args]) { |io| JSON.parse(io.read) }
|
15
10
|
end
|
16
11
|
|
12
|
+
QUERY = <<~EOQ.strip
|
13
|
+
query($userName: String!, $from: DateTime!) {
|
14
|
+
user(login: $userName) {
|
15
|
+
contributionsCollection(from: $from) {
|
16
|
+
contributionCalendar {
|
17
|
+
totalContributions
|
18
|
+
weeks {
|
19
|
+
contributionDays {
|
20
|
+
contributionCount
|
21
|
+
date
|
22
|
+
}
|
23
|
+
}
|
24
|
+
}
|
25
|
+
}
|
26
|
+
}
|
27
|
+
}
|
28
|
+
EOQ
|
29
|
+
|
17
30
|
def get name, year
|
18
|
-
|
19
|
-
path = ".#{name}.#{year}.html"
|
31
|
+
path = ".#{name}.#{year}.json"
|
20
32
|
|
21
33
|
unless File.exist? path then
|
22
34
|
warn "#{name} #{year}" if $v
|
23
35
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
36
|
+
data = graphql("--paginate", "--slurp",
|
37
|
+
"-F", "userName=#{name}",
|
38
|
+
"-F", "from=#{year}-01-01T00:00:00",
|
39
|
+
"-f", "query=%s" % [QUERY])
|
40
|
+
.dig(0, "data", "user", # JFC
|
41
|
+
"contributionsCollection", "contributionCalendar",
|
42
|
+
"weeks")
|
43
|
+
|
44
|
+
data = data.map { |h| h["contributionDays"] }
|
45
|
+
data = data.map { |a| a.to_h { |h| [h["date"], h["contributionCount"]] } }
|
46
|
+
data = data.reduce(&:merge).sort.to_h
|
47
|
+
data = data.select { |k,v| k.start_with? "#{year}" } # edges of calendar
|
48
|
+
data = data.select { |k,v| v > 0 }
|
49
|
+
|
50
|
+
File.write path, JSON.pretty_generate(data)
|
29
51
|
end
|
30
52
|
|
31
|
-
|
53
|
+
JSON.parse File.read path
|
54
|
+
end
|
55
|
+
|
56
|
+
YEARS_QUERY = <<~EOQ.strip
|
57
|
+
query($userName: String!) {
|
58
|
+
user(login: $userName) {
|
59
|
+
contributionsCollection {
|
60
|
+
contributionYears
|
61
|
+
}
|
62
|
+
}
|
63
|
+
}
|
64
|
+
EOQ
|
65
|
+
|
66
|
+
def years name
|
67
|
+
graphql("-F", "userName=#{name}", "-f", "query=%s" % [YEARS_QUERY])
|
68
|
+
.dig("data", "user", "contributionsCollection", "contributionYears")
|
69
|
+
end
|
70
|
+
|
71
|
+
def load_all name, years
|
72
|
+
years.map { |year| get name, year }.reduce(&:merge)
|
32
73
|
end
|
33
74
|
|
34
75
|
def generate name, last, io = $stdout, testing = false
|
35
|
-
|
36
|
-
|
37
|
-
<html lang="en">
|
38
|
-
EOM
|
76
|
+
last ||= years(name).min
|
77
|
+
last = last.to_i # string from cmdline
|
39
78
|
|
40
79
|
unless testing then
|
41
|
-
FileUtils.rm_f ".#{name}.#{Time.now.year}.
|
80
|
+
FileUtils.rm_f ".#{name}.#{Time.now.year}.json" # always fetch this fresh
|
42
81
|
end
|
43
|
-
html = get name, Time.now.year
|
44
82
|
|
45
|
-
|
46
|
-
io.puts %( <body>)
|
47
|
-
io.puts html.css("script").to_html
|
83
|
+
steps = 16
|
48
84
|
|
49
|
-
|
50
|
-
|
51
|
-
.at_css("div.graph-before-activity-overview")
|
85
|
+
# HACK: make it know the years automatically
|
86
|
+
contribs = load_all name, last..Time.now.year
|
52
87
|
|
53
|
-
|
88
|
+
d0, dN = Date.new(last), Date.today
|
54
89
|
|
55
|
-
|
56
|
-
.content = graph.previous.previous.content.strip.gsub(/\s+/, " ")
|
90
|
+
min, max = contribs.values.minmax
|
57
91
|
|
58
|
-
|
59
|
-
.remove_class("d-flex")
|
60
|
-
.remove_class("text-center")
|
61
|
-
.remove_class("flex-xl-items-center")
|
92
|
+
max1 = Math.log(max+1)
|
62
93
|
|
63
|
-
|
64
|
-
|
94
|
+
scale = ->(n) { [n, (steps * Math.log(n) / max1).floor] }
|
95
|
+
|
96
|
+
total_contributions = contribs.values.sum
|
97
|
+
|
98
|
+
range = (min..max) # used for legend below
|
99
|
+
.group_by { |n| scale[n].last }
|
100
|
+
.transform_values { |ary|
|
101
|
+
m, n = ary.minmax
|
102
|
+
m == n ? m : Range.new(m, n)
|
103
|
+
}
|
104
|
+
|
105
|
+
contribs.transform_values!(&scale)
|
106
|
+
|
107
|
+
years = (d0.year..dN.year).to_a.reverse.to_h { |year|
|
108
|
+
d0 = Date.new year
|
109
|
+
d1 = Date.new year+1
|
110
|
+
|
111
|
+
d0 -= d0.wday # back it up to sunday to square everything off
|
112
|
+
d1 += (7-d1.wday) # unless d1.wday == 0
|
113
|
+
|
114
|
+
days = (d0...d1).map { |d| d.year == year ? [d.to_s, contribs[d.to_s]] : [] }
|
115
|
+
|
116
|
+
by_week = days.each_slice(7).to_a.transpose
|
65
117
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
118
|
+
[ year, by_week ]
|
119
|
+
}
|
120
|
+
|
121
|
+
def io.td day, code, count
|
122
|
+
if code && count then
|
123
|
+
print '<td class="entry day green-color-%d tooltip">' % [code]
|
124
|
+
print '<div class="right">%s = %s</div>' % [day, count]
|
125
|
+
puts '</td>'
|
126
|
+
else
|
127
|
+
puts '<td class="entry day nocolor"></td>'
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
io.puts "<html>"
|
132
|
+
io.puts "<head>"
|
133
|
+
io.puts "<title>%s's contribution calendar</title>" % [name]
|
134
|
+
io.puts <<~CSS
|
135
|
+
<style>
|
136
|
+
body { background-color: #ffffff; font-family: system-ui; }
|
137
|
+
|
138
|
+
.entry {
|
139
|
+
font-size: 0.8rem;
|
140
|
+
display: inline-block;
|
141
|
+
margin: 1px;
|
142
|
+
width: 12px;
|
143
|
+
height: 12px;
|
144
|
+
border-radius: 2px;
|
145
|
+
outline: 1px solid rgba(0, 0, 0, 10%);
|
146
|
+
outline-offset: -1px;
|
147
|
+
}
|
148
|
+
|
149
|
+
.entry.day:hover { outline: 2px solid rgba(0, 0, 0, 10%); }
|
150
|
+
|
151
|
+
.non-day { outline: none; } /* cancel out entry's outline for non-days */
|
152
|
+
|
153
|
+
.tooltip {
|
154
|
+
display: inline-block;
|
155
|
+
position: relative;
|
156
|
+
}
|
157
|
+
|
158
|
+
.tooltip .right {
|
159
|
+
font-family: monospace;
|
160
|
+
font-size: 1.2rem;
|
161
|
+
min-width: 11em; # "yyyy-mm-dd = xyz" = 16 chars? but em calculation is wack
|
162
|
+
top: 50%;
|
163
|
+
left: 100%;
|
164
|
+
margin-left: 20px;
|
165
|
+
transform: translate(0, -);
|
166
|
+
padding: 5px 5px;
|
167
|
+
color: #444;
|
168
|
+
background-color: #ccc;
|
169
|
+
border-radius: 8px;
|
170
|
+
position: absolute;
|
171
|
+
z-index: 99999999;
|
172
|
+
box-sizing: border-box;
|
173
|
+
border: 1px solid #fff;
|
174
|
+
display: none;
|
175
|
+
}
|
176
|
+
|
177
|
+
.tooltip:hover .right { display: block; }
|
178
|
+
|
179
|
+
.nocolor { background-color: hsl(120, 10%, 99%); }
|
180
|
+
|
181
|
+
.green-color-0 { background-color: hsl(120 70% 95%); }
|
182
|
+
.green-color-1 { background-color: hsl(120 70% 90%); }
|
183
|
+
.green-color-2 { background-color: hsl(120 70% 85%); }
|
184
|
+
.green-color-3 { background-color: hsl(120 70% 80%); }
|
185
|
+
.green-color-4 { background-color: hsl(120 70% 75%); }
|
186
|
+
.green-color-5 { background-color: hsl(120 70% 70%); }
|
187
|
+
.green-color-6 { background-color: hsl(120 70% 65%); }
|
188
|
+
.green-color-7 { background-color: hsl(120 70% 60%); }
|
189
|
+
.green-color-8 { background-color: hsl(120 70% 55%); }
|
190
|
+
.green-color-9 { background-color: hsl(120 70% 50%); }
|
191
|
+
.green-color-10 { background-color: hsl(120 70% 45%); }
|
192
|
+
.green-color-11 { background-color: hsl(120 70% 40%); }
|
193
|
+
.green-color-12 { background-color: hsl(120 70% 35%); }
|
194
|
+
.green-color-13 { background-color: hsl(120 70% 30%); }
|
195
|
+
.green-color-14 { background-color: hsl(120 70% 25%); }
|
196
|
+
.green-color-15 { background-color: hsl(120 70% 20%); }
|
197
|
+
</style>
|
198
|
+
CSS
|
199
|
+
io.puts "</head>"
|
200
|
+
|
201
|
+
io.puts "<body>"
|
202
|
+
|
203
|
+
io.puts "<h1>#{name}'s complete github contributions</h1>"
|
204
|
+
io.puts "<p><small>Total contributions = %d</small>" % [total_contributions]
|
205
|
+
|
206
|
+
io.puts "<div><small>Legend: </small>"
|
207
|
+
io.puts "<table>"
|
208
|
+
io.puts "<tr>"
|
209
|
+
io.puts '<td class="entry day nocolor"></td>'
|
210
|
+
steps.times.each do |code|
|
211
|
+
io.td "level #{code}", code, range[code]
|
212
|
+
end
|
213
|
+
io.puts "</tr>"
|
214
|
+
io.puts "</table>"
|
215
|
+
io.puts "</div>"
|
216
|
+
|
217
|
+
years.each do |year, by_week|
|
218
|
+
total = contribs
|
219
|
+
.select { |date, (count, code)| date.start_with?(year.to_s) && count }
|
220
|
+
.values
|
221
|
+
.map(&:first)
|
222
|
+
.sum
|
223
|
+
|
224
|
+
io.puts "<h2>%d</h2>" % [year]
|
225
|
+
|
226
|
+
io.puts '<table class="heatmap calendar">'
|
227
|
+
|
228
|
+
by_week.each_with_index do |weekdays, idx|
|
229
|
+
io.puts "<!-- #{year} #{Date::ABBR_DAYNAMES[idx]} -->"
|
230
|
+
io.puts "<tr>"
|
231
|
+
|
232
|
+
io.puts '<td class="entry non-day">%s</td>' % [[1, 3, 5].include?(idx) ? Date::ABBR_DAYNAMES[idx][0] : nil]
|
233
|
+
|
234
|
+
weekdays.each_with_index do |(day, (count, code)), wday|
|
235
|
+
if day then
|
236
|
+
io.td day, code, count
|
237
|
+
else
|
238
|
+
io.puts '<td class="entry non-day"></td>'
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
io.puts "</tr>"
|
243
|
+
end
|
244
|
+
|
245
|
+
io.puts "</table>"
|
246
|
+
io.puts "<small>%d contributions</small>" % [total]
|
247
|
+
end # years
|
71
248
|
|
72
|
-
io.puts
|
73
|
-
|
74
|
-
</html>
|
75
|
-
EOM
|
249
|
+
io.puts "</body>"
|
250
|
+
io.puts "</html>"
|
76
251
|
end
|
77
252
|
end
|
@@ -2,23 +2,18 @@ require "minitest/autorun"
|
|
2
2
|
require "github_contribs"
|
3
3
|
|
4
4
|
class TestGithubContribs < Minitest::Test
|
5
|
-
def test_oauth_token
|
6
|
-
gh = GithubContribs.new
|
7
|
-
assert_match(/^gho_\w{36}/, gh.oauth_token, "if this fails, they all fail")
|
8
|
-
end
|
9
|
-
|
10
5
|
def test_generate
|
11
6
|
name = "zenspider"
|
12
7
|
last = Time.now.year
|
13
8
|
|
14
9
|
gh = GithubContribs.new
|
15
10
|
|
16
|
-
str = ""
|
11
|
+
str = +""
|
17
12
|
io = StringIO.new str
|
18
13
|
|
19
14
|
gh.generate name, last, io, :testing
|
20
15
|
|
21
|
-
assert_includes str, "<title>zenspider
|
22
|
-
assert_match(/<
|
16
|
+
assert_includes str, "<title>zenspider's contribution calendar</title>"
|
17
|
+
assert_match(/<table class="heatmap calendar">/, str)
|
23
18
|
end
|
24
19
|
end
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: github_contribs
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Davis
|
@@ -10,9 +10,9 @@ bindir: bin
|
|
10
10
|
cert_chain:
|
11
11
|
- |
|
12
12
|
-----BEGIN CERTIFICATE-----
|
13
|
-
|
13
|
+
MIIDPjCCAiagAwIBAgIBCDANBgkqhkiG9w0BAQsFADBFMRMwEQYDVQQDDApyeWFu
|
14
14
|
ZC1ydWJ5MRkwFwYKCZImiZPyLGQBGRYJemVuc3BpZGVyMRMwEQYKCZImiZPyLGQB
|
15
|
-
|
15
|
+
GRYDY29tMB4XDTI0MDEwMjIxMjEyM1oXDTI1MDEwMTIxMjEyM1owRTETMBEGA1UE
|
16
16
|
AwwKcnlhbmQtcnVieTEZMBcGCgmSJomT8ixkARkWCXplbnNwaWRlcjETMBEGCgmS
|
17
17
|
JomT8ixkARkWA2NvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALda
|
18
18
|
b9DCgK+627gPJkB6XfjZ1itoOQvpqH1EXScSaba9/S2VF22VYQbXU1xQXL/WzCkx
|
@@ -22,29 +22,15 @@ cert_chain:
|
|
22
22
|
qhtV7HJxNKuPj/JFH0D2cswvzznE/a5FOYO68g+YCuFi5L8wZuuM8zzdwjrWHqSV
|
23
23
|
gBEfoTEGr7Zii72cx+sCAwEAAaM5MDcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAw
|
24
24
|
HQYDVR0OBBYEFEfFe9md/r/tj/Wmwpy+MI8d9k/hMA0GCSqGSIb3DQEBCwUAA4IB
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
25
|
+
AQCygvpmncmkiSs9r/Kceo4bBPDszhTv6iBi4LwMReqnFrpNLMOWJw7xi8x+3eL2
|
26
|
+
XS09ZPNOt2zm70KmFouBMgOysnDY4k2dE8uF6B8JbZOO8QfalW+CoNBliefOTcn2
|
27
|
+
bg5IOP7UoGM5lC174/cbDJrJnRG9bzig5FAP0mvsgA8zgTRXQzIUAZEo92D5K7p4
|
28
|
+
B4/O998ho6BSOgYBI9Yk1ttdCtti6Y+8N9+fZESsjtWMykA+WXWeGUScHqiU+gH8
|
29
|
+
S7043fq9EbQdBr2AXdj92+CDwuTfHI6/Hj5FVBDULufrJaan4xUgL70Hvc6pTTeW
|
30
|
+
deKfBjgVAq7EYHu1AczzlUly
|
31
31
|
-----END CERTIFICATE-----
|
32
|
-
date:
|
32
|
+
date: 2024-07-01 00:00:00.000000000 Z
|
33
33
|
dependencies:
|
34
|
-
- !ruby/object:Gem::Dependency
|
35
|
-
name: nokogiri
|
36
|
-
requirement: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - "~>"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '1.12'
|
41
|
-
type: :runtime
|
42
|
-
prerelease: false
|
43
|
-
version_requirements: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '1.12'
|
48
34
|
- !ruby/object:Gem::Dependency
|
49
35
|
name: rdoc
|
50
36
|
requirement: !ruby/object:Gem::Requirement
|
@@ -71,14 +57,14 @@ dependencies:
|
|
71
57
|
requirements:
|
72
58
|
- - "~>"
|
73
59
|
- !ruby/object:Gem::Version
|
74
|
-
version: '
|
60
|
+
version: '4.2'
|
75
61
|
type: :development
|
76
62
|
prerelease: false
|
77
63
|
version_requirements: !ruby/object:Gem::Requirement
|
78
64
|
requirements:
|
79
65
|
- - "~>"
|
80
66
|
- !ruby/object:Gem::Version
|
81
|
-
version: '
|
67
|
+
version: '4.2'
|
82
68
|
description: |-
|
83
69
|
A simple commandline tool that downloads yearly contribution graphs
|
84
70
|
from github and assembles them into a unified view.
|
@@ -122,7 +108,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
122
108
|
- !ruby/object:Gem::Version
|
123
109
|
version: '0'
|
124
110
|
requirements: []
|
125
|
-
rubygems_version: 3.
|
111
|
+
rubygems_version: 3.5.14
|
126
112
|
signing_key:
|
127
113
|
specification_version: 4
|
128
114
|
summary: A simple commandline tool that downloads yearly contribution graphs from
|
metadata.gz.sig
CHANGED
Binary file
|