github_contribs 1.1.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f41a12f72aa1ff4448d1a8f11fd4f6ef48dcd995b388ce64a5495fce0ae08d33
4
- data.tar.gz: 208e49020c9aff47bd8e4c914aca0e17547e934176633ebaa219f8ea05c8a6fa
3
+ metadata.gz: 77816e7a617476af27562ba5ec816996cf8a965780c8c317d8d44fc48f6e4758
4
+ data.tar.gz: 3b448123b525319b886f64b0b7f12f9945b2b4595f57634d53b56e8ba724a8a1
5
5
  SHA512:
6
- metadata.gz: d97e88c845240284bc97cc2a0ed748dade907810a90c2572f38ce9af0ff0242ddbb86fa3842bfbe42a4e9647cc44917300f861797997d45cb75ac0bed7b0ccc8
7
- data.tar.gz: 4a9dd171a59a58f4a0eb14d0a3acbd8998a33e74ba09edd709a687dae99b34bdeae6f166b0c60928dfde0691b75122e7c81468d7ec3d00cef81726a2b525bc70
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 1998"
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
@@ -9,7 +9,7 @@ if ARGV.empty? then
9
9
  end
10
10
 
11
11
  name = ARGV.shift
12
- last = (ARGV.shift || (Time.now.year - 10)).to_i
12
+ last = ARGV.shift
13
13
 
14
14
  gh = GithubContribs.new
15
15
 
@@ -1,77 +1,252 @@
1
- require "pp"
2
- require "nokogiri"
3
1
  require "fileutils"
4
- require "open-uri"
5
- require "yaml"
2
+ require "json"
3
+ require "date"
6
4
 
7
5
  class GithubContribs
8
- VERSION = "1.1.0"
6
+ VERSION = "2.0.0"
9
7
 
10
- def oauth_token
11
- @token ||= ENV["GITHUB_TOKEN"] || begin
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
- base_url = "https://github.com/#{name}?"
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
- uri = URI.parse "#{base_url}from=%4d-01-01&to=%4d-12-31" % [year, year]
25
-
26
- File.open path, "w" do |f|
27
- f.puts uri.read("authorization" => "Bearer #{oauth_token}")
28
- end
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
- Nokogiri::HTML File.read path
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
- io.puts <<~EOM
36
- <!DOCTYPE html>
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}.html" # always fetch this fresh
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
- io.puts html.at_css("head").to_html
46
- io.puts %( <body>)
47
- io.puts html.css("script").to_html
83
+ steps = 16
48
84
 
49
- Time.now.year.downto(last).each do |year|
50
- graph = get(name, year)
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
- graph.css("div.float-right").remove # NEW!...
88
+ d0, dN = Date.new(last), Date.today
54
89
 
55
- graph.css("div.float-left").first # Learn how we count...
56
- .content = graph.previous.previous.content.strip.gsub(/\s+/, " ")
90
+ min, max = contribs.values.minmax
57
91
 
58
- graph.at_css("div.js-calendar-graph")
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
- io.puts graph.to_html
64
- end # years
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
- io.puts <<~EOM
67
- <div class="Popover js-hovercard-content position-absolute" style="display: none; outline: none;" tabindex="0">
68
- <div class="Popover-message Popover-message--bottom-left Popover-message--large Box box-shadow-large" style="width:360px;"></div>
69
- </div>
70
- EOM
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 <<~EOM
73
- </body>
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 (Ryan Davis) · GitHub</title>"
22
- assert_match(/<svg width="\d+" height="\d+" class="js-calendar-graph-svg">/, str)
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: 1.1.0
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
- MIIDPjCCAiagAwIBAgIBBjANBgkqhkiG9w0BAQsFADBFMRMwEQYDVQQDDApyeWFu
13
+ MIIDPjCCAiagAwIBAgIBCDANBgkqhkiG9w0BAQsFADBFMRMwEQYDVQQDDApyeWFu
14
14
  ZC1ydWJ5MRkwFwYKCZImiZPyLGQBGRYJemVuc3BpZGVyMRMwEQYKCZImiZPyLGQB
15
- GRYDY29tMB4XDTIxMTIyMzIzMTkwNFoXDTIyMTIyMzIzMTkwNFowRTETMBEGA1UE
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
- AQCKB5jfsuSnKb+t/Wrh3UpdkmX7TrEsjVmERC0pPqzQ5GQJgmEXDD7oMgaKXaAq
26
- x2m+KSZDrqk7c8uho5OX6YMqg4KdxehfSLqqTZGoeV78qwf/jpPQZKTf+W9gUSJh
27
- zsWpo4K50MP+QtdSbKXZwjAafpQ8hK0MnnZ/aeCsW9ov5vdXpYbf3dpg6ADXRGE7
28
- lQY2y1tJ5/chqu6h7dQmnm2ABUqx9O+JcN9hbCYoA5i/EeubUEtFIh2w3SpO6YfB
29
- JFmxn4h9YO/pVdB962BdBNNDia0kgIjI3ENnkLq0dKpYU3+F3KhEuTksLO0L6X/V
30
- YsuyUzsMz6GQA4khyaMgKNSD
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: 2022-10-05 00:00:00.000000000 Z
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: '3.25'
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: '3.25'
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.3.12
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