github_contribs 1.1.0 → 2.0.1

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