hubba 0.4.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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