hubba 0.3.0 → 0.6.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
  SHA1:
3
- metadata.gz: 990a81f63411ae5b97cd3ae1e6892cb8e8f788d3
4
- data.tar.gz: 1061e2b8963f97072cbdcedb5c6ba8c23ac3b691
3
+ metadata.gz: 63505774303ed6881c5650478eb76a1df7a3d412
4
+ data.tar.gz: 517173da9e008bd5d9ea74d2e9d8d1622edf7fdf
5
5
  SHA512:
6
- metadata.gz: 05efa27d809de02a2e5582abe3c0d9b8f70629246d6d973d640b3fbb648365d7e0c6ecf32359837b06a83b2607d1ed4d9b5799af9c0f27d4244cce4dfe235a23
7
- data.tar.gz: 9ad616f2209a6f1c566b895d0e908b5a1cc5706d85626eb0519bb74117d4a6b4dc2f71c40eac2adb3e7caa1cfe4af33cfd1f7de57d96dd2d2390126a99e58899
6
+ metadata.gz: 8a806d4ee314cc13c1605781cbc6af7192600d2c34ec9ebccc949541a800a41d93d43def28600aa0d27e7526a47afa1e319bc2184ec4cee1c917196cc080ca91
7
+ data.tar.gz: bcc42747f508c0acab55a8f652be1103afef20d9409172081bba34a530a9bbc441a2d5ec109bcd4b43e3049a3b3f74fc1c478d17e9c84a7e7ce41e7dabd11014
File without changes
@@ -1,16 +1,18 @@
1
- HISTORY.md
1
+ CHANGELOG.md
2
2
  Manifest.txt
3
3
  README.md
4
4
  Rakefile
5
5
  lib/hubba.rb
6
- lib/hubba/cache.rb
7
6
  lib/hubba/client.rb
7
+ lib/hubba/config.rb
8
8
  lib/hubba/github.rb
9
9
  lib/hubba/stats.rb
10
10
  lib/hubba/version.rb
11
- test/cache/users~geraldb~orgs.json
12
- test/cache/users~geraldb~repos.json
13
11
  test/helper.rb
14
- test/test_cache.rb
12
+ test/stats/jekyll~minima.json
13
+ test/stats/openblockchains~awesome-blockchains.json
14
+ test/stats/opendatajson~factbook.json.json
15
+ test/stats/poole~hyde.json
15
16
  test/test_config.rb
16
17
  test/test_stats.rb
18
+ test/test_stats_tmp.rb
data/README.md CHANGED
@@ -1,20 +1,20 @@
1
- # hubba
2
-
3
- hubba gem - (yet) another (lite) GitHub HTTP API client / library
4
-
5
- * home :: [github.com/gittiscripts/hubba](https://github.com/gittiscripts/hubba)
6
- * bugs :: [github.com/gittiscripts/hubba/issues](https://github.com/gittiscripts/hubba/issues)
7
- * gem :: [rubygems.org/gems/hubba](https://rubygems.org/gems/hubba)
8
- * rdoc :: [rubydoc.info/gems/hubba](http://rubydoc.info/gems/hubba)
9
-
10
-
11
- ## Usage
12
-
13
- TBD
14
-
15
-
16
-
17
- ## License
18
-
19
- The `hubba` scripts are dedicated to the public domain.
20
- Use it as you please with no restrictions whatsoever.
1
+ # hubba
2
+
3
+ hubba gem - (yet) another (lite) GitHub HTTP API client / library
4
+
5
+ * home :: [github.com/rubycoco/git](https://github.com/rubycoco/git)
6
+ * bugs :: [github.com/rubycoco/git/issues](https://github.com/rubycoco/git/issues)
7
+ * gem :: [rubygems.org/gems/hubba](https://rubygems.org/gems/hubba)
8
+ * rdoc :: [rubydoc.info/gems/hubba](http://rubydoc.info/gems/hubba)
9
+
10
+
11
+ ## Usage
12
+
13
+ TBD
14
+
15
+
16
+
17
+ ## License
18
+
19
+ The `hubba` scripts are dedicated to the public domain.
20
+ Use it as you please with no restrictions whatsoever.
data/Rakefile CHANGED
@@ -5,26 +5,26 @@ Hoe.spec 'hubba' do
5
5
 
6
6
  self.version = Hubba::VERSION
7
7
 
8
- self.summary = 'hubba - (yet) another (lite) GitHub HTTP API client / library'
8
+ self.summary = 'hubba - (yet) another (lite) GitHub HTTP API client / library'
9
9
  self.description = summary
10
10
 
11
- self.urls = ['https://github.com/gittiscripts/hubba']
11
+ self.urls = { home: 'https://github.com/rubycoco/git' }
12
12
 
13
13
  self.author = 'Gerald Bauer'
14
14
  self.email = 'ruby-talk@ruby-lang.org'
15
15
 
16
16
  # switch extension to .markdown for gihub formatting
17
17
  self.readme_file = 'README.md'
18
- self.history_file = 'HISTORY.md'
18
+ self.history_file = 'CHANGELOG.md'
19
19
 
20
20
  self.extra_deps = [
21
- ['logutils' ],
21
+ ['webclient', '>= 0.1.1']
22
22
  ]
23
23
 
24
24
  self.licenses = ['Public Domain']
25
25
 
26
26
  self.spec_extras = {
27
- required_ruby_version: '>= 2.3'
27
+ required_ruby_version: '>= 2.2.2'
28
28
  }
29
29
 
30
30
  end
@@ -1,25 +1,20 @@
1
- # encoding: utf-8
2
-
3
- require 'net/http'
4
- require "net/https"
5
- require 'uri'
6
-
7
- require 'pp'
8
- require 'json'
9
- require 'yaml'
10
- require 'time'
11
-
12
-
13
- # 3rd party gems/libs
14
- require 'logutils'
1
+ # 3rd party (our own)
2
+ require 'webclient'
15
3
 
16
4
  # our own code
17
5
  require 'hubba/version' # note: let version always go first
18
- require 'hubba/cache'
6
+ require 'hubba/config'
19
7
  require 'hubba/client'
20
8
  require 'hubba/github'
21
9
  require 'hubba/stats'
22
10
 
23
11
 
12
+ ############
13
+ # add convenience alias for alternate spelling - why? why not?
14
+ module Hubba
15
+ GitHub = Github
16
+ end
17
+
18
+
24
19
  # say hello
25
- puts Hubba.banner if defined?($RUBYLIBS_DEBUG)
20
+ puts Hubba.banner if defined?($RUBYCOCO_DEBUG)
@@ -1,53 +1,79 @@
1
- # encoding: utf-8
2
-
3
1
  module Hubba
4
2
 
5
3
 
6
4
  class Client
7
5
 
8
- def initialize( user: nil, password: nil )
9
- uri = URI.parse( "https://api.github.com" )
10
- @http = Net::HTTP.new(uri.host, uri.port)
11
- @http.use_ssl = true
12
- @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
6
+ BASE_URL = 'https://api.github.com'
7
+
8
+
9
+ def initialize( token: nil,
10
+ user: nil, password: nil )
11
+ ## add support for (personal access) token
12
+ @token = token
13
13
 
14
14
  ## add support for basic auth - defaults to no auth (nil/nil)
15
+ ## remove - deprecated (use token) - why? why not?
15
16
  @user = user ## use login like Oktokit - why? why not?
16
17
  @password = password
17
18
  end # method initialize
18
19
 
20
+
21
+
19
22
  def get( request_uri )
20
23
  puts "GET #{request_uri}"
21
24
 
22
- req = Net::HTTP::Get.new( request_uri )
23
- ## req = Net::HTTP::Get.new( "/users/geraldb" )
24
- ## req = Net::HTTP::Get.new( "/users/geraldb/repos" )
25
- req["User-Agent"] = "ruby/hubba" ## required by GitHub API
26
- req["Accept" ] = "application/vnd.github.v3+json" ## recommend by GitHub API
25
+ ## note: request_uri ALWAYS starts with leading /, thus use + for now!!!
26
+ # e.g. /users/geraldb
27
+ # /users/geraldb/repos
28
+ url = BASE_URL + request_uri
29
+
30
+ headers = {}
31
+ headers['User-Agent'] = 'ruby/hubba' ## required by GitHub API
32
+ headers['Accept'] = 'application/vnd.github.v3+json' ## recommend by GitHub API
27
33
 
34
+ auth = []
28
35
  ## check if credentials (user/password) present - if yes, use basic auth
29
- if @user && @password
36
+ if @token
37
+ puts " using (personal access) token - starting with: #{@token[0..6]}**********"
38
+ headers['Authorization'] = "token #{@token}"
39
+ ## token works like:
40
+ ## curl -H 'Authorization: token my_access_token' https://api.github.com/user/repos
41
+ elsif @user && @password
30
42
  puts " using basic auth - user: #{@user}, password: ***"
31
- req.basic_auth( @user, @password )
43
+ ## use credential auth "tuple" (that is, array with two string items) for now
44
+ ## or use Webclient::HttpBasicAuth or something - why? why not?
45
+ auth = [@user, @password]
46
+ # req.basic_auth( @user, @password )
47
+ else
48
+ puts " using no credentials (no token, no user/password)"
32
49
  end
33
50
 
34
- res = @http.request(req)
51
+ res = Webclient.get( url,
52
+ headers: headers,
53
+ auth: auth )
35
54
 
36
55
  # Get specific header
37
56
  # response["content-type"]
38
57
  # => "text/html; charset=UTF-8"
39
58
 
40
59
  # Iterate all response headers.
41
- res.each_header do |key, value|
42
- p "#{key} => #{value}"
60
+ puts "HTTP HEADERS:"
61
+ res.headers.each do |key, value|
62
+ puts " #{key}: >#{value}<"
43
63
  end
64
+ puts
65
+
44
66
  # => "location => http://www.google.com/"
45
67
  # => "content-type => text/html; charset=UTF-8"
46
68
  # ...
47
69
 
48
- json = JSON.parse( res.body )
49
- ## pp json
50
- json
70
+ if res.status.ok?
71
+ res.json
72
+ else
73
+ puts "!! HTTP ERROR: #{res.status.code} #{res.status.message}:"
74
+ pp res.raw
75
+ exit 1
76
+ end
51
77
  end # methdo get
52
78
 
53
79
  end ## class Client
@@ -0,0 +1,46 @@
1
+ module Hubba
2
+
3
+ class Configuration
4
+ def data_dir() @data_dir || './data'; end
5
+ def data_dir=( value ) @data_dir = value; end
6
+
7
+ # try default setup via ENV variables
8
+ def token() @token || ENV[ 'HUBBA_TOKEN' ]; end
9
+ def token=( value ) @token = value; end
10
+
11
+ # todo/check: use HUBBA_LOGIN - why? why not?
12
+ def user() @user || ENV[ 'HUBBA_USER' ]; end
13
+ def password() @password || ENV[ 'HUBBA_PASSWORD' ]; end
14
+ def user=( value ) @user = value; end
15
+ def password=( value ) @password = value; end
16
+
17
+ end # class Configuration
18
+
19
+
20
+ ## lets you use
21
+ ## Hubba.configure do |config|
22
+ ## config.token = 'secret'
23
+ ## -or-
24
+ ## config.user = 'testdada'
25
+ ## config.password = 'secret'
26
+ ## end
27
+ ##
28
+ ## move configure block to GitHub class - why? why not?
29
+ ## e.g. GitHub.configure do |config|
30
+ ## ...
31
+ ## end
32
+
33
+
34
+ def self.configuration
35
+ @configuration ||= Configuration.new
36
+ end
37
+ class << self
38
+ alias_method :config, :configuration
39
+ end
40
+
41
+
42
+ def self.configure
43
+ yield( configuration )
44
+ end
45
+
46
+ end # module Hubba
@@ -1,38 +1,5 @@
1
- # encoding: utf-8
2
-
3
1
  module Hubba
4
2
 
5
- class Configuration
6
- attr_accessor :user
7
- attr_accessor :password
8
-
9
- def initialize
10
- # try default setup via ENV variables
11
- @user = ENV[ 'HUBBA_USER' ] ## use HUBBA_LOGIN - why? why not?
12
- @password = ENV[ 'HUBBA_PASSWORD' ]
13
- end
14
- end
15
-
16
- ## lets you use
17
- ## Hubba.configure do |config|
18
- ## config.user = 'testdada'
19
- ## config.password = 'secret'
20
- ## end
21
- ##
22
- ## move configure block to GitHub class - why? why not?
23
- ## e.g. GitHub.configure do |config|
24
- ## ...
25
- ## end
26
-
27
-
28
- def self.configuration
29
- @configuration ||= Configuration.new
30
- end
31
-
32
- def self.configure
33
- yield( configuration )
34
- end
35
-
36
3
 
37
4
  class Resource
38
5
  attr_reader :data
@@ -53,25 +20,25 @@ class Orgs < Resource
53
20
  ## sort by name
54
21
  data.map { |item| item['login'] }.sort
55
22
  end
23
+ alias_method :names, :logins ## add name alias - why? why not?
56
24
  end
57
25
 
58
26
 
59
27
 
60
28
  class Github
61
29
 
62
- def initialize( cache_dir: './cache' )
63
- @cache = Cache.new( cache_dir )
64
- @client = Client.new( user: Hubba.configuration.user,
65
- password: Hubba.configuration.password )
66
-
67
- @offline = false
30
+ def initialize
31
+ @client = if Hubba.configuration.token
32
+ Client.new( token: Hubba.configuration.token )
33
+ elsif Hubba.configuration.user &&
34
+ Hubba.configuration.password
35
+ Client.new( user: Hubba.configuration.user,
36
+ password: Hubba.configuration.password )
37
+ else
38
+ Client.new
39
+ end
68
40
  end
69
41
 
70
- def offline!() @offline = true; end ## switch to offline - todo: find a "better" way - why? why not?
71
- def online!() @offline = false; end
72
- def offline?() @offline == true; end
73
- def online?() @offline == false; end
74
-
75
42
 
76
43
  def user( name )
77
44
  Resource.new( get "/users/#{name}" )
@@ -95,6 +62,7 @@ def user_orgs( name )
95
62
  end
96
63
 
97
64
 
65
+
98
66
  def org( name )
99
67
  Resource.new( get "/orgs/#{name}" )
100
68
  end
@@ -114,15 +82,28 @@ def repo_commits( full_name )
114
82
  end
115
83
 
116
84
 
117
- private
118
- def get( request_uri )
119
- if offline?
120
- @cache.get( request_uri )
85
+
86
+ ####
87
+ # more
88
+ def update( obj )
89
+ if obj.is_a?( Stats )
90
+ full_name = obj.full_name
91
+ puts "[update 1/2] fetching repo >#{full_name}<..."
92
+ repo = repo( full_name )
93
+ puts "[update 2/2] fetching repo >#{full_name}< commits ..."
94
+ commits = repo_commits( full_name )
95
+
96
+ obj.update( repo, commits )
121
97
  else
122
- @client.get( request_uri )
98
+ raise ArgumentError, "unknown source object passed in - expected Hubba::Stats; got #{obj.class.name}"
123
99
  end
124
100
  end
125
101
 
126
- end # class Github
127
102
 
128
- end # module Hubba
103
+ private
104
+ def get( request_uri )
105
+ @client.get( request_uri )
106
+ end
107
+
108
+ end # class Github
109
+ end # module Hubba
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  module Hubba
4
2
 
5
3
  ####
@@ -12,47 +10,225 @@ module Hubba
12
10
  def initialize( full_name )
13
11
  @data = {}
14
12
  @data['full_name'] = full_name # e.g. poole/hyde etc.
13
+
14
+ @cache = {}
15
15
  end
16
16
 
17
- def full_name() @data['full_name']; end
18
17
 
19
- def created_at() @data['created_at']; end
20
- def updated_at() @data['updated_at']; end
21
- def pushed_at() @data['pushed_at']; end
18
+ def full_name() @data['full_name']; end
19
+
22
20
 
23
- def history() @data['history']; end
21
+ ## note: return datetime objects (NOT strings); if not present/available return nil/null
22
+ def created_at() @cache['created_at'] ||= parse_datetime( @data['created_at'] ); end
23
+ def updated_at() @cache['updated_at'] ||= parse_datetime( @data['updated_at'] ); end
24
+ def pushed_at() @cache['pushed_at'] ||= parse_datetime( @data['pushed_at'] ); end
24
25
 
26
+ ## date (only) versions
27
+ def created() @cache['created'] ||= parse_date( @data['created_at'] ); end
28
+ def updated() @cache['updated'] ||= parse_date( @data['updated_at'] ); end
29
+ def pushed() @cache['pushed'] ||= parse_date( @data['pushed_at'] ); end
25
30
 
26
31
  def size
27
32
  # size of repo in kb (as reported by github api)
28
33
  @data['size'] || 0 ## return 0 if not found - why? why not? (return nil - why? why not??)
29
34
  end
30
35
 
36
+
37
+ def history
38
+ @cache['history'] ||= begin
39
+ if @data['history']
40
+ build_history( @data['history'] )
41
+ else
42
+ nil
43
+ end
44
+ end
45
+ end
46
+
47
+
31
48
  def stars
32
49
  ## return last stargazers_count entry (as number; 0 if not found)
33
- t1 = 0
50
+ @cache['stars'] ||= history ? history[0].stars : 0
51
+ end
52
+
53
+
54
+ def commits() @data['commits']; end
55
+
56
+ def last_commit ## convenience shortcut; get first/last commit (use [0]) or nil
57
+ if @data['commits'] && @data['commits'][0]
58
+ @data['commits'][0]
59
+ else
60
+ nil
61
+ end
62
+ end
63
+
64
+
65
+ def committed ## last commit date (from author NOT committer)
66
+ @cache['committed'] ||= parse_date( last_commit_author_date )
67
+ end
68
+
69
+ def committed_at() ## last commit date (from author NOT committer)
70
+ @cache['committed_at'] ||= parse_datetime( last_commit_author_date )
71
+ end
72
+
73
+ def last_commit_author_date
74
+ h = last_commit
75
+ h ? h['author']['date'] : nil
76
+ end
77
+
78
+
79
+ def last_commit_message ## convenience shortcut; last commit message
80
+ h = last_commit
81
+
82
+ committer_name = h['committer']['name']
83
+ author_name = h['author']['name']
84
+ message = h['message']
85
+
86
+ buf = ""
87
+ buf << message
88
+ buf << " by #{author_name}"
89
+
90
+ if committer_name != author_name
91
+ buf << " w/ #{committer_name}"
92
+ end
93
+ end # method commit_message
94
+
95
+
96
+
97
+ ###
98
+ # helpers
99
+ def parse_datetime( str ) str ? DateTime.strptime( str, '%Y-%m-%dT%H:%M:%S') : nil; end
100
+ def parse_date( str ) str ? Date.strptime( str, '%Y-%m-%d') : nil; end
101
+
102
+ ########
103
+ ## build history items (structs)
104
+
105
+ class HistoryItem
106
+
107
+ attr_reader :date, :stars ## read-only attributes
108
+ attr_accessor :prev, :next ## read/write attributes (for double linked list/nodes/items)
109
+
110
+ def initialize( date:, stars: )
111
+ @date = date
112
+ @stars = stars
113
+ @next = nil
114
+ end
115
+
116
+ ## link items (append item at the end/tail)
117
+ def append( item )
118
+ @next = item
119
+ item.prev = self
120
+ end
121
+
122
+ def diff_days
123
+ if @next
124
+ ## note: use jd=julian days for calculation
125
+ @date.jd - @next.date.jd
126
+ else
127
+ nil ## last item (tail)
128
+ end
129
+ end
130
+
131
+ def diff_stars
132
+ if @next
133
+ @stars - @next.stars
134
+ else
135
+ nil ## last item (tail)
136
+ end
137
+ end
138
+ end ## class HistoryItem
139
+
140
+
141
+ def build_history( timeseries )
142
+ items = []
143
+
144
+ keys = timeseries.keys.sort.reverse ## newest (latest) items first
145
+ keys.each do |key|
146
+ h = timeseries[ key ]
147
+
148
+ item = HistoryItem.new(
149
+ date: Date.strptime( key, '%Y-%m-%d' ),
150
+ stars: h['stargazers_count'] || 0 )
151
+
152
+ ## link items
153
+ last_item = items[-1]
154
+ last_item.append( item ) if last_item ## if not nil? append (note first item has no prev item)
155
+
156
+ items << item
157
+ end
158
+
159
+ ## todo/check: return [] for empty items array (items.empty?) - why?? why not??
160
+ if items.empty?
161
+ nil
162
+ else
163
+ items
164
+ end
165
+ end ## method build_history
34
166
 
35
- if history
36
- history_keys = history.keys.sort.reverse
37
- ## todo/fix: for now assumes one entry per week
38
- ## simple case [0] and [1] for a week later
39
- ## check actual date - why? why not?
40
- stats_t1 = history_keys[0] ? history[ history_keys[0] ] : nil
41
- if stats_t1
42
- t1 = stats_t1['stargazers_count'] || 0
43
- end
44
- end
45
- t1
46
- end # method stars
167
+
168
+
169
+ def calc_diff_stars( samples: 3, days: 30 )
170
+ ## samples: use n history item samples e.g. 3 samples
171
+ ## days e.g. 7 days (per week), 30 days (per month)
172
+
173
+ if history.nil?
174
+ nil ## todo/check: return 0.0 too - why? why not?
175
+ elsif history.size == 1
176
+ ## just one item; CANNOT calc diff; return zero
177
+ 0.0
178
+ else
179
+ idx = [history.size, samples].min ## calc last index
180
+ last = history[idx-1]
181
+ first = history[0]
182
+
183
+ diff_days = first.date.jd - last.date.jd
184
+ diff_stars = first.stars - last.stars
185
+
186
+ ## note: use factor 1000 for fixed integer division
187
+ ## converts to float at the end
188
+
189
+ ## todo: check for better way (convert to float upfront - why? why not?)
190
+
191
+ diff = (diff_stars * days * 1000) / diff_days
192
+ puts "diff=#{diff}:#{diff.class.name}" ## check if it's a float
193
+ (diff.to_f/1000.0)
194
+ end
195
+ end
196
+
197
+
198
+ def history_str ## todo/check: rename/change to format_history or fmt_history - why? why not?
199
+ ## returns "pretty printed" history as string buffer
200
+ buf = ''
201
+ buf << "[#{history.size}]: "
202
+
203
+ history.each do |item|
204
+ buf << "#{item.stars}"
205
+
206
+ diff_stars = item.diff_stars
207
+ diff_days = item.diff_days
208
+ if diff_stars && diff_days ## note: last item has no diffs
209
+ if diff_stars > 0 || diff_stars < 0
210
+ if diff_stars > 0
211
+ buf << " (+#{diff_stars}"
212
+ else
213
+ buf << " (#{diff_stars}"
214
+ end
215
+ buf << " in #{diff_days}d) "
216
+ else ## diff_stars == 0
217
+ buf << " (#{diff_days}d) "
218
+ end
219
+ end
220
+ end
221
+ buf
222
+ end # method history_str
47
223
 
48
224
 
49
225
 
50
226
  ###############################
51
227
  ## fetch / read / write methods
52
228
 
53
- def fetch( gh ) ## update stats / fetch data from github via api
54
- puts "fetching #{full_name}..."
55
- repo = gh.repo( full_name )
229
+ def update( repo, commits ) ## update stats / fetch data from github via api
230
+ raise ArgumentError, "Hubba::Resource expected; got #{repo.class.name}" unless repo.is_a?( Resource )
231
+ raise ArgumentError, "Hubba::Resource expected; got #{commits.class.name}" unless commits.is_a?( Resource )
56
232
 
57
233
  ## e.g. 2015-05-11T20:21:43Z
58
234
  ## puts Time.iso8601( repo.data['created_at'] )
@@ -77,7 +253,6 @@ module Hubba
77
253
 
78
254
  ##########################
79
255
  ## also check / keep track of (latest) commit
80
- commits = gh.repo_commits( full_name )
81
256
  puts "last commit/update:"
82
257
  ## pp commits
83
258
  commit = {
@@ -85,36 +260,58 @@ module Hubba
85
260
  'date' => commits.data[0]['commit']['committer']['date'],
86
261
  'name' => commits.data[0]['commit']['committer']['name']
87
262
  },
263
+ 'author' => {
264
+ 'date' => commits.data[0]['commit']['author']['date'],
265
+ 'name' => commits.data[0]['commit']['author']['name']
266
+ },
88
267
  'message' => commits.data[0]['commit']['message']
89
268
  }
90
269
 
91
270
  ## for now store only the latest commit (e.g. a single commit in an array)
92
- @data[ 'commits'] = [commit]
271
+ @data[ 'commits' ] = [commit]
93
272
 
94
273
  pp @data
274
+
275
+ ## reset (invalidate) cached values from data hash
276
+ ## use after reading or fetching
277
+ @cache = {}
278
+
279
+ self ## return self for (easy chaining)
95
280
  end
96
281
 
97
282
 
98
283
 
99
- def write( data_dir: './data' )
284
+ def write
100
285
  basename = full_name.gsub( '/', '~' ) ## e.g. poole/hyde become poole~hyde
101
- puts "writing (saving) to #{basename}..."
286
+ data_dir = Hubba.config.data_dir
287
+ puts "writing stats to #{basename} (#{data_dir})..."
288
+
289
+ ## todo/fix: add FileUtils.makepath_r or such!!!
102
290
  File.open( "#{data_dir}/#{basename}.json", 'w:utf-8' ) do |f|
103
291
  f.write JSON.pretty_generate( data )
104
292
  end
293
+ self ## return self for (easy chaining)
105
294
  end
106
295
 
107
- def read( data_dir: './data' ) ## note: use read instead of load (load is kind of keyword for loading code)
296
+
297
+ def read
108
298
  ## note: skip reading if file not present
109
299
  basename = full_name.gsub( '/', '~' ) ## e.g. poole/hyde become poole~hyde
110
- filename = "#{data_dir}/#{basename}.json"
111
- if File.exist?( filename )
112
- puts "reading (loading) from #{basename}..."
113
- json = File.open( filename, 'r:utf-8' ) { |file| file.read } ## todo/fix: use read_utf8
300
+ data_dir = Hubba.config.data_dir
301
+ path = "#{data_dir}/#{basename}.json"
302
+
303
+ if File.exist?( path )
304
+ puts "reading stats from #{basename} (#{data_dir})..."
305
+ json = File.open( path, 'r:utf-8' ) { |f| f.read }
114
306
  @data = JSON.parse( json )
307
+
308
+ ## reset (invalidate) cached values from data hash
309
+ ## use after reading or fetching
310
+ @cache = {}
115
311
  else
116
- puts "skipping reading (loading) from #{basename} -- file not found"
312
+ puts "!! WARN: - skipping reading stats from #{basename} -- file not found"
117
313
  end
314
+ self ## return self for (easy chaining)
118
315
  end
119
316
 
120
317
  end # class Stats