hubba 0.4.0 → 0.6.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.
@@ -0,0 +1,249 @@
1
+ module Hubba
2
+
3
+
4
+ class Report
5
+ def initialize( stats_or_hash_or_path=Hubba.stats )
6
+ ## puts "[debug] Report#initialize:"
7
+ ## pp stats_or_hash_or_path if stats_or_hash_or_path.is_a?( String )
8
+
9
+ @stats = if stats_or_hash_or_path.is_a?( String ) ||
10
+ stats_or_hash_or_path.is_a?( Hash )
11
+ hash_or_path = stats_or_hash_or_path
12
+ Hubba.stats( hash_or_path )
13
+ else
14
+ stats_or_hash_or_path ## assume Summary/Stats - todo/fix: double check!!!
15
+ end
16
+ end
17
+
18
+ def save( path )
19
+ buf = build
20
+ puts "writing report >#{path}< ..."
21
+ File.open( path, "w:utf-8" ) do |f|
22
+ f.write( buf )
23
+ end
24
+ end
25
+ end ## class Report
26
+
27
+
28
+
29
+ class ReportSummary < Report
30
+
31
+ def build
32
+ ## create a (summary report)
33
+ ##
34
+ ## add stars, last_updates, etc.
35
+ ## org description etc??
36
+
37
+ ## note: orgs is orgs+users e.g. geraldb, yorobot etc
38
+ buf = String.new('')
39
+ buf << "# #{@stats.repos.size} repos @ #{@stats.orgs.size} orgs\n"
40
+ buf << "\n"
41
+
42
+
43
+ @stats.orgs.each do |org|
44
+ name = org[0]
45
+ repos = org[1]
46
+ buf << "### #{name} _(#{repos.size})_\n"
47
+ buf << "\n"
48
+
49
+ ### add stats for repos
50
+ entries = []
51
+ repos.each do |repo|
52
+ entries << "**#{repo.name}** ★#{repo.stats.stars} (#{repo.stats.size} kb)"
53
+ end
54
+
55
+ buf << entries.join( ' · ' ) ## use interpunct? - was: • (bullet)
56
+ buf << "\n"
57
+ buf << "\n"
58
+ end
59
+
60
+ buf
61
+ end # method build
62
+ end # class ReportSummary
63
+
64
+
65
+
66
+ class ReportStars < Report
67
+
68
+ def build
69
+
70
+ ## add stars, last_updates, etc.
71
+ ## org description etc??
72
+
73
+ ## note: orgs is orgs+users e.g. geraldb, yorobot etc
74
+ buf = String.new('')
75
+ buf << "# #{@stats.repos.size} repos @ #{@stats.orgs.size} orgs\n"
76
+ buf << "\n"
77
+
78
+
79
+ repos = @stats.repos.sort do |l,r|
80
+ ## note: use reverse sort (right,left) - e.g. most stars first
81
+ r.stats.stars <=> l.stats.stars
82
+ end
83
+
84
+ ## pp repos
85
+
86
+ repos.each_with_index do |repo,i|
87
+ buf << "#{i+1}. ★#{repo.stats.stars} **#{repo.full_name}** (#{repo.stats.size} kb)\n"
88
+ end
89
+
90
+ buf
91
+ end # method build
92
+ end # class ReportStars
93
+
94
+
95
+
96
+ class ReportTimeline < Report
97
+
98
+ def build
99
+ ## create a (timeline report)
100
+
101
+ ## note: orgs is orgs+users e.g. geraldb, yorobot etc
102
+ buf = String.new('')
103
+ buf << "# #{@stats.repos.size} repos @ #{@stats.orgs.size} orgs\n"
104
+ buf << "\n"
105
+
106
+
107
+ repos = @stats.repos.sort do |l,r|
108
+ ## note: use reverse sort (right,left) - e.g. most stars first
109
+ ## r[:stars] <=> l[:stars]
110
+
111
+ ## sort by created_at (use julian days)
112
+ r.stats.created.jd <=> l.stats.created.jd
113
+ end
114
+
115
+
116
+ ## pp repos
117
+
118
+
119
+ last_year = -1
120
+ last_month = -1
121
+
122
+ repos.each_with_index do |repo,i|
123
+ year = repo.stats.created.year
124
+ month = repo.stats.created.month
125
+
126
+ if last_year != year
127
+ buf << "\n## #{year}\n\n"
128
+ end
129
+
130
+ if last_month != month
131
+ buf << "\n### #{month}\n\n"
132
+ end
133
+
134
+ last_year = year
135
+ last_month = month
136
+
137
+ buf << "- #{repo.stats.created_at.strftime('%Y-%m-%d')} ★#{repo.stats.stars} **#{repo.full_name}** (#{repo.stats.size} kb)\n"
138
+ end
139
+
140
+ buf
141
+ end # method build
142
+ end # class ReportTimeline
143
+
144
+
145
+
146
+ class ReportTrending < Report
147
+
148
+ def build
149
+
150
+ ## note: orgs is orgs+users e.g. geraldb, yorobot etc
151
+ buf = String.new('')
152
+ buf << "# #{@stats.repos.size} repos @ #{@stats.orgs.size} orgs\n"
153
+ buf << "\n"
154
+
155
+ ###
156
+ ## todo:
157
+ ## use calc per month (days: 30)
158
+ ## per week is too optimistic (e.g. less than one star/week e.g. 0.6 or something)
159
+
160
+ repos = @stats.repos.sort do |l,r|
161
+ ## note: use reverse sort (right,left) - e.g. most stars first
162
+ ## r[:stars] <=> l[:stars]
163
+
164
+ ## sort by created_at (use julian days)
165
+ ## r[:created_at].to_date.jd <=> l[:created_at].to_date.jd
166
+
167
+ res = r.diff <=> l.diff
168
+ res = r.stats.stars <=> l.stats.stars if res == 0
169
+ res = r.stats.created.jd <=> l.stats.created.jd if res == 0
170
+ res
171
+ end
172
+
173
+
174
+ ## pp repos
175
+
176
+
177
+ repos.each_with_index do |repo,i|
178
+ if repo.diff == 0
179
+ buf << "- -/- "
180
+ else
181
+ buf << "- #{repo.diff}/month "
182
+ end
183
+
184
+ buf << " ★#{repo.stats.stars} **#{repo.full_name}** (#{repo.stats.size} kb) - "
185
+ buf << "#{repo.stats.history_str}\n"
186
+ end
187
+
188
+
189
+ buf
190
+ end # method build
191
+ end # class ReportTrending
192
+
193
+
194
+
195
+ class ReportUpdates < Report
196
+
197
+ def build
198
+
199
+ ## note: orgs is orgs+users e.g. geraldb, yorobot etc
200
+ buf = String.new('')
201
+ buf << "# #{@stats.repos.size} repos @ #{@stats.orgs.size} orgs\n"
202
+ buf << "\n"
203
+
204
+ repos = @stats.repos.sort do |l,r|
205
+ r.stats.committed.jd <=> l.stats.committed.jd
206
+ end
207
+
208
+ ## pp repos
209
+
210
+
211
+ buf << "committed / pushed / updated / created\n\n"
212
+
213
+ today = Date.today
214
+
215
+ repos.each_with_index do |repo,i|
216
+
217
+ days_ago = today.jd - repo.stats.committed.jd
218
+
219
+ diff1 = repo.stats.committed.jd - repo.stats.pushed.jd
220
+ diff2 = repo.stats.committed.jd - repo.stats.updated.jd
221
+ diff3 = repo.stats.pushed.jd - repo.stats.updated.jd
222
+
223
+ buf << "- (#{days_ago}d) **#{repo.full_name}** ★#{repo.stats.stars} - "
224
+ buf << "#{repo.stats.committed} "
225
+ buf << "("
226
+ buf << (diff1==0 ? '=' : "#{diff1}d")
227
+ buf << "/"
228
+ buf << (diff2==0 ? '=' : "#{diff2}d")
229
+ buf << ")"
230
+ buf << " / "
231
+ buf << "#{repo.stats.pushed} "
232
+ buf << "("
233
+ buf << (diff3==0 ? '=' : "#{diff3}d")
234
+ buf << ")"
235
+ buf << " / "
236
+ buf << "#{repo.stats.updated} / "
237
+ buf << "#{repo.stats.created} - "
238
+ buf << "‹#{repo.stats.last_commit_message}›"
239
+ buf << " (#{repo.stats.size} kb)"
240
+ buf << "\n"
241
+ end
242
+
243
+
244
+ buf
245
+ end # method build
246
+ end # class ReportUpdates
247
+
248
+
249
+ end # module Hubba
@@ -0,0 +1,170 @@
1
+ module Hubba
2
+
3
+
4
+ def self.update_stats( host_or_path='./repos.yml' ) ## move to reposet e.g. Reposet#update_status!!!!
5
+ h = if hash_or_path.is_a?( String ) ## assume it is a file path!!!
6
+ path = hash_or_path
7
+ YAML.load_file( path )
8
+ else
9
+ hash_or_path # assume its a hash / reposet already!!!
10
+ end
11
+
12
+ gh = Github.new
13
+ gh.update_stats( h )
14
+ end
15
+
16
+
17
+ def self.stats( hash_or_path='./repos.yml' ) ## use read_stats or such - why? why not?
18
+ h = if hash_or_path.is_a?( String ) ## assume it is a file path!!!
19
+ path = hash_or_path
20
+ YAML.load_file( path )
21
+ else
22
+ hash_or_path # assume its a hash / reposet already!!!
23
+ end
24
+
25
+ Summary.new( h ) ## wrap in "easy-access" facade / wrapper
26
+ end
27
+
28
+
29
+
30
+ class Summary # todo/check: use a different name e.g (Data)Base, Census, Catalog, Collection, Index, Register or such???
31
+
32
+ class Repo ## (nested) class
33
+
34
+ attr_reader :owner,
35
+ :name
36
+
37
+ def initialize( owner, name )
38
+ @owner = owner ## rename to login, username - why? why not?
39
+ @name = name ## rename to reponame ??
40
+ end
41
+
42
+ def full_name() "#{owner}/#{name}"; end
43
+
44
+ def stats
45
+ ## note: load stats on demand only (first access) for now - why? why not?
46
+ @stats ||= begin
47
+ stats = Stats.new( full_name )
48
+ stats.read
49
+ stats
50
+ end
51
+ end
52
+
53
+ def diff
54
+ @diff ||= stats.calc_diff_stars( samples: 3, days: 30 )
55
+ end
56
+ end # (nested) class Repo
57
+
58
+
59
+ attr_reader :orgs, :repos
60
+
61
+ def initialize( hash )
62
+ @orgs = [] # orgs and users -todo/check: use better name - logins or owners? why? why not?
63
+ @repos = []
64
+ add( hash )
65
+
66
+ puts "#{@repos.size} repos @ #{@orgs.size} orgs"
67
+ end
68
+
69
+ #############
70
+ ## private helpes
71
+ def add( hash ) ## add repos.yml set
72
+ hash.each do |org_with_counter, names|
73
+ ## remove optional number from key e.g.
74
+ ## mrhydescripts (3) => mrhydescripts
75
+ ## footballjs (4) => footballjs
76
+ ## etc.
77
+ org = org_with_counter.sub( /\([0-9]+\)/, '' ).strip
78
+ repos = []
79
+ names.each do |name|
80
+ repo = Repo.new( org, name )
81
+ repos << repo
82
+ end
83
+ @orgs << [org, repos]
84
+ @repos += repos
85
+ end
86
+ end
87
+ end # class Summary
88
+
89
+
90
+
91
+ ## orgs - include repos form org(anizations) too
92
+ ## cache - save json response to cache_dir - change to/use debug/tmp_dir? - why? why not?
93
+ def self.reposet( *users, orgs: true,
94
+ cache: false )
95
+ # users = [users] if users.is_a?( String ) ### wrap in array if single user
96
+
97
+ gh = Hubba::Github.new
98
+
99
+ forks = []
100
+
101
+ h = {}
102
+ users.each do |user|
103
+ res = gh.user_repos( user )
104
+ save_json( "#{config.cache_dir}/users~#{user}~repos.json", res.data ) if cache
105
+
106
+ repos = []
107
+ ####
108
+ # check for forked repos (auto-exclude personal by default)
109
+ # note: forked repos in orgs get NOT auto-excluded!!!
110
+ res.data.each do |repo|
111
+ fork = repo['fork']
112
+ if fork
113
+ print "FORK "
114
+ forks << "#{repo['full_name']} (AUTO-EXCLUDED)"
115
+ else
116
+ print " "
117
+ repos << repo['name']
118
+ end
119
+ print repo['full_name']
120
+ print "\n"
121
+ end
122
+
123
+
124
+ h[ "#{user} (#{repos.size})" ] = repos.sort
125
+ end
126
+
127
+
128
+ ## all repos from orgs
129
+ ## note: for now only use first (primary user) - why? why not?
130
+ if orgs
131
+ user = users[0]
132
+ res = gh.user_orgs( user )
133
+ save_json( "#{config.cache_dir}/users~#{user}~orgs.json", res.data ) if cache
134
+
135
+
136
+ logins = res.logins.each do |login|
137
+ ## next if ['xxx'].include?( login ) ## add orgs here to skip
138
+
139
+ res = gh.org_repos( login )
140
+ save_json( "#{config.cache_dir}/orgs~#{login}~repos.json", res.data ) if cache
141
+
142
+ repos = []
143
+ res.data.each do |repo|
144
+ fork = repo['fork']
145
+ if fork
146
+ print "FORK "
147
+ forks << repo['full_name']
148
+ repos << repo['name']
149
+ else
150
+ print " "
151
+ repos << repo['name']
152
+ end
153
+ print repo['full_name']
154
+ print "\n"
155
+ end
156
+
157
+ h[ "#{login} (#{repos.size})" ] = repos.sort
158
+ end
159
+ end
160
+
161
+ if forks.size > 0
162
+ puts
163
+ puts "#{forks.size} fork(s):"
164
+ puts forks
165
+ end
166
+
167
+ h
168
+ end ## method reposet
169
+ end # module Hubba
170
+
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  module Hubba
4
2
 
5
3
  ####
@@ -12,39 +10,95 @@ 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
17
 
18
- def full_name() @full_name ||= @data['full_name']; end
18
+ def full_name() @data['full_name']; end
19
+
19
20
 
20
21
  ## note: return datetime objects (NOT strings); if not present/available return nil/null
21
- def created_at() @created_at ||= @data['created_at'] ? DateTime.strptime( @data['created_at'], '%Y-%m-%dT%H:%M:%S') : nil; end
22
- def updated_at() @updated_at ||= @data['updated_at'] ? DateTime.strptime( @data['updated_at'], '%Y-%m-%dT%H:%M:%S') : nil; end
23
- def pushed_at() @pushed_at ||= @data['pushed_at'] ? DateTime.strptime( @data['pushed_at'], '%Y-%m-%dT%H:%M:%S') : nil; end
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
 
25
- def history() @history ||= @data['history'] ? build_history( @data['history'] ) : nil; end
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
26
30
 
27
31
  def size
28
32
  # size of repo in kb (as reported by github api)
29
- @size ||= @data['size'] || 0 ## return 0 if not found - why? why not? (return nil - why? why not??)
33
+ @data['size'] || 0 ## return 0 if not found - why? why not? (return nil - why? why not??)
34
+ end
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
30
45
  end
31
46
 
47
+
32
48
  def stars
33
49
  ## return last stargazers_count entry (as number; 0 if not found)
34
- @stars ||= history ? history[0].stars : 0
50
+ @cache['stars'] ||= history ? history[0].stars : 0
35
51
  end
36
52
 
37
- def reset_cache
38
- ## reset (invalidate) cached values from data hash
39
- ## use after reading or fetching
40
- @full_name = nil
41
- @created_at = @updated_at = @pushed_at = nil
42
- @history = nil
43
- @size = nil
44
- @stars = nil
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
45
62
  end
46
63
 
47
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
+
48
102
  ########
49
103
  ## build history items (structs)
50
104
 
@@ -56,6 +110,7 @@ module Hubba
56
110
  def initialize( date:, stars: )
57
111
  @date = date
58
112
  @stars = stars
113
+ @next = nil
59
114
  end
60
115
 
61
116
  ## link items (append item at the end/tail)
@@ -134,12 +189,13 @@ module Hubba
134
189
  ## todo: check for better way (convert to float upfront - why? why not?)
135
190
 
136
191
  diff = (diff_stars * days * 1000) / diff_days
137
- puts "diff=#{diff}:#{diff.class.name}" ## check if it's a float
192
+ ## puts "diff=#{diff}:#{diff.class.name}" ## check if it's a float
138
193
  (diff.to_f/1000.0)
139
194
  end
140
195
  end
141
196
 
142
- def history_str
197
+
198
+ def history_str ## todo/check: rename/change to format_history or fmt_history - why? why not?
143
199
  ## returns "pretty printed" history as string buffer
144
200
  buf = ''
145
201
  buf << "[#{history.size}]: "
@@ -166,12 +222,13 @@ module Hubba
166
222
  end # method history_str
167
223
 
168
224
 
225
+
169
226
  ###############################
170
227
  ## fetch / read / write methods
171
228
 
172
- def fetch( gh ) ## update stats / fetch data from github via api
173
- puts "fetching #{full_name}..."
174
- 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 )
175
232
 
176
233
  ## e.g. 2015-05-11T20:21:43Z
177
234
  ## puts Time.iso8601( repo.data['created_at'] )
@@ -196,7 +253,6 @@ module Hubba
196
253
 
197
254
  ##########################
198
255
  ## also check / keep track of (latest) commit
199
- commits = gh.repo_commits( full_name )
200
256
  puts "last commit/update:"
201
257
  ## pp commits
202
258
  commit = {
@@ -204,6 +260,10 @@ module Hubba
204
260
  'date' => commits.data[0]['commit']['committer']['date'],
205
261
  'name' => commits.data[0]['commit']['committer']['name']
206
262
  },
263
+ 'author' => {
264
+ 'date' => commits.data[0]['commit']['author']['date'],
265
+ 'name' => commits.data[0]['commit']['author']['name']
266
+ },
207
267
  'message' => commits.data[0]['commit']['message']
208
268
  }
209
269
 
@@ -212,15 +272,21 @@ module Hubba
212
272
 
213
273
  pp @data
214
274
 
215
- reset_cache
275
+ ## reset (invalidate) cached values from data hash
276
+ ## use after reading or fetching
277
+ @cache = {}
278
+
216
279
  self ## return self for (easy chaining)
217
280
  end
218
281
 
219
282
 
220
283
 
221
- def write( data_dir: './data' )
284
+ def write
222
285
  basename = full_name.gsub( '/', '~' ) ## e.g. poole/hyde become poole~hyde
223
- puts "writing stats 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!!!
224
290
  File.open( "#{data_dir}/#{basename}.json", 'w:utf-8' ) do |f|
225
291
  f.write JSON.pretty_generate( data )
226
292
  end
@@ -228,17 +294,22 @@ module Hubba
228
294
  end
229
295
 
230
296
 
231
- def read( data_dir: './data' )
297
+ def read
232
298
  ## note: skip reading if file not present
233
299
  basename = full_name.gsub( '/', '~' ) ## e.g. poole/hyde become poole~hyde
234
- filename = "#{data_dir}/#{basename}.json"
235
- if File.exist?( filename )
236
- puts "reading stats from #{basename}..."
237
- 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 }
238
306
  @data = JSON.parse( json )
239
- reset_cache
307
+
308
+ ## reset (invalidate) cached values from data hash
309
+ ## use after reading or fetching
310
+ @cache = {}
240
311
  else
241
- puts "skipping reading stats from #{basename} -- file not found"
312
+ puts "!! WARN: - skipping reading stats from #{basename} -- file not found"
242
313
  end
243
314
  self ## return self for (easy chaining)
244
315
  end