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 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