diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..51d76d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +db/my.db diff --git a/Gemfile b/Gemfile index b01ffb2..60e004d 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,7 @@ source "http://gemcutter.org" gem "addressable", "2.2.5" gem "bcrypt-ruby", "2.1.4" -gem "bundler", "1.0.7" +gem "bundler", ">= 1.0.7" gem "configuration", "1.2.0" gem "data_mapper", "1.1.0" gem "data_objects", "0.10.5" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..d01b526 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,127 @@ +GEM + remote: http://rubygems.org/ + remote: http://rubygems.org/ + remote: http://gems.rubyforge.org/ + remote: http://gems.rubyonrails.org/ + remote: http://gems.github.com/ + remote: http://gemcutter.org/ + specs: + addressable (2.2.5) + bcrypt-ruby (2.1.4) + configuration (1.2.0) + data_mapper (1.1.0) + dm-aggregates (= 1.1.0) + dm-constraints (= 1.1.0) + dm-core (= 1.1.0) + dm-migrations (= 1.1.0) + dm-serializer (= 1.1.0) + dm-timestamps (= 1.1.0) + dm-transactions (= 1.1.0) + dm-types (= 1.1.0) + dm-validations (= 1.1.0) + data_objects (0.10.5) + addressable (~> 2.1) + dm-aggregates (1.1.0) + dm-core (~> 1.1.0) + dm-constraints (1.1.0) + dm-core (~> 1.1.0) + dm-core (1.1.0) + addressable (~> 2.2.4) + dm-do-adapter (1.1.0) + data_objects (~> 0.10.2) + dm-core (~> 1.1.0) + dm-migrations (1.1.0) + dm-core (~> 1.1.0) + dm-mysql-adapter (1.1.0) + dm-do-adapter (~> 1.1.0) + do_mysql (~> 0.10.2) + dm-serializer (1.1.0) + dm-core (~> 1.1.0) + fastercsv (~> 1.5.4) + json (~> 1.4.6) + dm-sqlite-adapter (1.1.0) + dm-do-adapter (~> 1.1.0) + do_sqlite3 (~> 0.10.2) + dm-timestamps (1.1.0) + dm-core (~> 1.1.0) + dm-transactions (1.1.0) + dm-core (~> 1.1.0) + dm-types (1.1.0) + bcrypt-ruby (~> 2.1.4) + dm-core (~> 1.1.0) + fastercsv (~> 1.5.4) + json (~> 1.4.6) + stringex (~> 1.2.0) + uuidtools (~> 2.1.2) + dm-validations (1.1.0) + dm-core (~> 1.1.0) + do_mysql (0.10.5) + data_objects (= 0.10.5) + do_sqlite3 (0.10.5) + data_objects (= 0.10.5) + fastercsv (1.5.4) + heroku (2.1.4) + launchy (>= 0.3.2) + rest-client (~> 1.6.1) + term-ansicolor (~> 1.0.5) + json (1.4.6) + launchy (0.4.0) + configuration (>= 0.0.5) + rake (>= 0.8.1) + mime-types (1.16) + mysql (2.8.1) + rack (1.2.2) + rake (0.8.7) + rest-client (1.6.1) + mime-types (>= 1.16) + sinatra (1.2.6) + rack (~> 1.1) + tilt (>= 1.2.2, < 2.0) + sqlite3 (1.3.3) + sqlite3-ruby (1.3.3) + sqlite3 (>= 1.3.3) + stringex (1.2.1) + term-ansicolor (1.0.5) + tilt (1.3) + uuidtools (2.1.2) + +PLATFORMS + ruby + +DEPENDENCIES + addressable (= 2.2.5) + bcrypt-ruby (= 2.1.4) + bundler (>= 1.0.7) + configuration (= 1.2.0) + data_mapper (= 1.1.0) + data_objects (= 0.10.5) + dm-aggregates (= 1.1.0) + dm-constraints (= 1.1.0) + dm-core (= 1.1.0) + dm-do-adapter (= 1.1.0) + dm-migrations (= 1.1.0) + dm-mysql-adapter (= 1.1.0) + dm-serializer (= 1.1.0) + dm-sqlite-adapter (= 1.1.0) + dm-timestamps (= 1.1.0) + dm-transactions (= 1.1.0) + dm-types (= 1.1.0) + dm-validations (= 1.1.0) + do_mysql (= 0.10.5) + do_sqlite3 (= 0.10.5) + fastercsv (= 1.5.4) + heroku (= 2.1.4) + json (= 1.4.6) + launchy (= 0.4.0) + mime-types (= 1.16) + mysql (= 2.8.1) + rack (= 1.2.2) + rake (= 0.8.7) + rest-client (= 1.6.1) + sinatra (= 1.2.6) + sqlite3 (= 1.3.3) + sqlite3-ruby (= 1.3.3) + stringex (= 1.2.1) + term-ansicolor (= 1.0.5) + tilt (= 1.3) + uuidtools (= 2.1.2) diff --git a/README.md b/README.md index a7407cb..85e5aba 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,37 @@ Github High Scores is a fun way to rank Github repository contributors in a 8-bit, 80's-tastic viewing environment. +## iCalendar + +This is an extension to convert the most recent commit log of a repo into [iCalendar](http://tools.ietf.org/html/rfc5545) format, to be included into iCal. + +This will generate events which are 30 minutes in duration -- there is no really easy way to specify a duration for a commit. + +To use: + http://example.com//.ics + http://example.com///.ics + +branch defaults to master, if not provided. + ## Installation git clone git://github.com/leereilly/github-high-scores.git + rvm install ruby-1.8.7-p334 ## didn't have correct version cd github-high-scores - ruby app.rb + gem install bundler + bundle + db_use=sqlite_default ruby app.rb ## Configuration -You'll need to set environment variables on your box. Locally, you can set them in your .bash_profile... +You can either use mysql, sqlite or sqlite with default setting + +For mysql: + +You'll need to set environment variables on your box. Locally, you can +set them in your .bash_profile... + export db_use=mysql export db_user=some_username export db_pass=some_password export db_host=some_host @@ -19,7 +40,20 @@ You'll need to set environment variables on your box. Locally, you can set them On Heroku, you can do something like this... - heroku config:add db_user=XXX db_pass=XXX db_host=XXX db_data=XXX + heroku config:add db_user=XXX db_pass=XXX db_host=XXX db_data=XXX db_use=mysql + +For sqlite: + + export db_use=sqlite + export db_path=/some/absolute/path/to/my.db + +For sqlite + default: + + export db_use=sqlite_default + +This then creates a my.db database file in db/ + +See app.rb for details. ## Contribute @@ -42,4 +76,4 @@ Fork + pull. * The GitHub API only allows 60 requests per minute per IP address. * The site looks like some sort of 80s-tastic arcade theme. -![Bugs](http://i.imgur.com/K8vsw.gif "Bugs") \ No newline at end of file +![Bugs](http://i.imgur.com/K8vsw.gif "Bugs") diff --git a/app.rb b/app.rb index 3e85ad0..b2303c3 100644 --- a/app.rb +++ b/app.rb @@ -6,20 +6,34 @@ require 'json' require 'erb' require 'uri' - +require 'data_mapper' +require 'dm-migrations' + +DataMapper::Logger.new($stdout, :debug) +dburl = case ENV['db_use'] + when 'mysql' then ("mysql://#{ENV['db_user']}:#{ENV['db_pass']}@"+ + "#{ENV['db_host']}/#{ENV['db_data']}") + when 'sqlite' then "sqlite3://#{ENV['db_path']}" + when 'sqlite_default' then "sqlite3://#{Dir.pwd}/db/my.db" + else + puts "REQUIRED: please provide a value for db_use -- see README.md" + exit + end +DataMapper.setup(:default, dburl) + +require 'helpers' require 'User' require 'Repo' - -DataMapper::Logger.new(STDOUT, :debug) +require 'Commit' disable :show_exceptions set :environment, :production - + error do @title = "404" @text = "Sorry, but this cat is in another castle!" @display_small_search = false - erb :not_found + erb :not_found end get '/' do @@ -30,7 +44,7 @@ @user = get_user_from_github_url(@github_url) @repo = get_repo_from_github_url(@github_url) @high_scores = get_high_scores(@user, @repo) - @display_small_search = true + @display_small_search = true redirect "/#{@user}/#{@repo}/high_scores/" else @title = 'High Scores' @@ -47,27 +61,42 @@ end get '/recent_searches/?' do - @repos = Repo.all(:limit => 5, :order => [ :updated_at.desc ]) + @repos = Repo.all(:limit => 5, :order => [ :updated_at.desc ]) puts @repos.inspect @display_small_search = true erb :recent_searches end - get '/credits/?' do erb :credits end get '/help/?' do - @display_small_search = true + @display_small_search = true erb :help end get '/about/?' do - @display_small_search = true + @display_small_search = true erb :about end +get '/:user/:repo/:branch.ics' do + @branch = params[:branch] + @user = User::create_from_username(params[:user]) + @repo = Repo::create_from_username_and_repo(params[:user], params[:repo]) + @commits = Commit::find_for(@repo, @branch) + [200, { "Content-Type"=> "text/calendar; charset=UTF-8" }, erb(:ical)] +end + +get '/:user/:repo.ics' do + @branch = "master" + @user = User::create_from_username(params[:user]) + @repo = Repo::create_from_username_and_repo(params[:user], params[:repo]) + @commits = Commit::find_for(@repo, @branch) + [200, { "Content-Type"=> "text/calendar; charset=UTF-8" }, erb(:ical)] +end + get '/:user/:repo/?' do @user = User::create_from_username(params[:user]) @repo = Repo::create_from_username_and_repo(params[:user], params[:repo]) @@ -87,33 +116,33 @@ erb :not_found end -def sanitize_input(url) +def sanitize_input(url) url = url.downcase - + # Special rules for Github URLs starting with 'github.com' if url[0..9] == 'github.com' url = 'https://www.github.com' + url[9..url.size] - + # Special rules for Github URLs starting with 'www.github.com' elsif url[0..13] == 'www.github.com' url = 'https://www.github.com' + url[13..url.size] end - + # Special rules for Github URLs ending in 'git' if url[-4,4] == '.git' url = url[0..-5] end - + url = url.gsub("http://", "https://") url = url.gsub("git@github.com:", "https://www.github.com/") url = url.gsub("git://", "https://www.") - + # If someone just passes in user/repo e.g. leereilly/leereilly.net tokens = url.split('/') if tokens.size == 2 url = "https://www.github.com/#{tokens[0]}/#{tokens[1]}" end - + return url end @@ -132,7 +161,7 @@ def get_high_scores(user, repo) puts "Storing user: #{stored_user}" stored_repo = Repo::create_from_username_and_repo(user, repo) puts "Storing repo: #{stored_repo}" - + contributors_url = "http://github.com/api/v2/json/repos/show/#{user}/#{repo}/contributors" contributors_feed = Net::HTTP.get_response(URI.parse(contributors_url)) @@ -149,7 +178,7 @@ def get_high_scores(user, repo) user_hash[:location] = repository_contributor['location'] user_hash[:contributions] = repository_contributor['contributions'].to_i contributors_array << user_hash - end + end return contributors_array rescue raise "Sorry, this GitHub repository doesn't seem to exist or is private" diff --git a/lib/Commit.rb b/lib/Commit.rb new file mode 100644 index 0000000..cf3181e --- /dev/null +++ b/lib/Commit.rb @@ -0,0 +1,61 @@ +require 'rubygems' +require 'data_mapper' +require 'net/http' +require 'json' +require 'uri' + +# This will probably blow away the database, so regenerate on each request! +# Caching is not really possible since the content of each page will change +# page=1 always being the most recent. +class Commit < BaseModel + + def initialize(json_data) + @json = json_data + end + + def to_ical + dtstart = DateTime.parse(@json['committed_date']).new_offset(0) + dtstamp = DateTime.now.new_offset(0) + url = "https://github.com%s" % @json["url"] + summary = "Commit by %s (%s)" % [@json["author"]["name"], @json["author"]["email"]] + + (<<-EOF).remove_indent + "\n" + BEGIN:VEVENT + SEQUENCE:1 + TRANSP:OPAQUE + UID:#{@json["id"]} + DTSTART:#{dtstart.ical_timestamp} + DTSTAMP:#{dtstamp.ical_timestamp} + SUMMARY:#{summary} + DESCRIPTION:#{@json['message'].gsub(/\n/,'\n')} + CREATED:#{dtstamp.ical_timestamp} + DTEND:#{(dtstart + 1/48.0).ical_timestamp} + LOCATION:#{url} + END:VEVENT + EOF + end + + def self.find_for(repo_obj, branch_name, get_all=false) + commits, page_num = [], 1 + loop do + jsonstr = get_json_response(github_api_url(repo_obj.owner, repo_obj.name, + branch_name, page_num)).body + objs = JSON.parse(jsonstr) + break if objs["error"] + objs["commits"].each { |commit| commits << Commit.new(commit) } + get_all ? page_num += 1 : break + end + commits + end + + def self.github_api_url(username, reponame, branch=nil, page_num=nil) + COMMITS_BASE_URL + ("%s/%s/%s?page=%d" % [username, reponame, + branch || "master", page_num || 1]) + end + + def self.get_json_response(url) + Net::HTTP.get_response(URI.parse(url)) + end +end + + diff --git a/lib/Contributor.rb b/lib/Contributor.rb index 5b06396..5208785 100644 --- a/lib/Contributor.rb +++ b/lib/Contributor.rb @@ -4,41 +4,34 @@ require 'json' require 'uri' -DataMapper::Logger.new($stdout, :debug) -puts "mysql://#{ENV['db_user']}:#{ENV['db_pass']}@#{ENV['db_host']}/#{ENV['db_data']}" -DataMapper.setup(:default, "mysql://#{ENV['db_user']}:#{ENV['db_pass']}@#{ENV['db_host']}/#{ENV['db_data']}") - -class Contributor +class Contributor < BaseModel include DataMapper::Resource - - API_VERSION = 'v2' - BASE_URL = 'http://github.com/api/' + API_VERSION + '/json/user/show/' - + property :id, Serial property :login, String property :gravatar_id, String property :contributions, String - + def self.create_from_user_and_repo(user, repo) stored_user = User::create_from_username(user) stored_repo = Repo::create_from_username_and_repo(user, repo) - - contributors_url = "http://github.com/api/v2/json/repos/show/#{user}/#{repo}/contributors" + + contributors_url = REPO_BASE_URL + "#{user}/#{repo}/contributors" contributors_feed = Net::HTTP.get_response(URI.parse(contributors_url)) contributors = contributors_feed.body contributors_result = JSON.parse(contributors) repository_contributors = contributors_result['contributors'] contributors_array = Array.new - + repository_contributors.each do |repository_contributor| contributor = Contributor.new contributor.login = repository_contributor['login'] contributor.gravatar_id = repository_contributor['gravatar_id'] contributor.contributions = repository_contributor['contributions'] contributor.save - end + end end - + def self.get_json_response(url) Net::HTTP.get_response(URI.parse(url)) end diff --git a/lib/Repo.rb b/lib/Repo.rb index df2683a..683c6ed 100644 --- a/lib/Repo.rb +++ b/lib/Repo.rb @@ -3,16 +3,10 @@ require 'net/http' require 'json' require 'uri' +require 'uuidtools' -DataMapper::Logger.new($stdout, :debug) -puts "mysql://#{ENV['db_user']}:#{ENV['db_pass']}@#{ENV['db_host']}/#{ENV['db_data']}" -DataMapper.setup(:default, "mysql://#{ENV['db_user']}:#{ENV['db_pass']}@#{ENV['db_host']}/#{ENV['db_data']}") - -class Repo +class Repo < BaseModel include DataMapper::Resource - - API_VERSION = 'v2' - BASE_URL = 'http://github.com/api/' + API_VERSION + '/json/repos/show/' property :id, Serial, :lazy => false property :owner, Text, :lazy => false @@ -20,10 +14,10 @@ class Repo property :homepage, Text, :lazy => false property :name, Text, :lazy => false property :description, Text , :lazy => false - property :parent, Text, :lazy => false - property :has_issues, Text, :lazy => false + property :parent, Text, :lazy => false + property :has_issues, Text, :lazy => false property :source, Text, :lazy => false - property :watchers, Text, :lazy => false + property :watchers, Text, :lazy => false property :has_downloads, Text, :lazy => false property :fork, Text, :lazy => false property :forks, Text, :lazy => false @@ -31,11 +25,15 @@ class Repo property :pushed_at, Text, :lazy => false property :open_issues, Text, :lazy => false property :updated_at, DateTime, :lazy => false - + + def ical_uuid + UUIDTools::UUID.sha1_create(UUIDTools::UUID_DNS_NAMESPACE, self.url).to_s.upcase + end + def self.create_from_username_and_repo(username, repo) repo_data_url = Repo.get_repo_data_url(username, repo) - - if found_repo = Repo.first(:owner => username, :name => repo) + + if found_repo = Repo.first(:owner => username, :name => repo) if Time.now - Time.parse(found_repo.updated_at.to_s) <= 60*60*24 puts "Repo created less than 24 hours ago. Returning DB record" return found_repo @@ -47,11 +45,11 @@ def self.create_from_username_and_repo(username, repo) puts "User not found; using web services" repo = Repo.new end - + repo_data_response = get_json_response(repo_data_url) repo_data = JSON.parse(repo_data_response.body) repo_data = repo_data['repository'] - + repo.owner = repo_data['owner'] repo.name = repo_data['name'] repo.url = repo_data['url'] @@ -66,18 +64,18 @@ def self.create_from_username_and_repo(username, repo) repo.forks = repo_data['forks'] repo.has_wiki = repo_data['has_wiki'] repo.pushed_at = repo_data['pushed_at'] - repo.open_issues = repo_data['open_issues'] - repo.updated_at = Time.now + repo.open_issues = repo_data['open_issues'] + repo.updated_at = Time.now repo.save! return repo end - + def self.get_json_response(url) Net::HTTP.get_response(URI.parse(url)) end def self.get_repo_data_url(username, repo) - return BASE_URL + username + '/' + repo + return REPO_BASE_URL + username + '/' + repo end end diff --git a/lib/User.rb b/lib/User.rb index 2d63b5a..2382ce8 100644 --- a/lib/User.rb +++ b/lib/User.rb @@ -4,23 +4,16 @@ require 'json' require 'uri' -DataMapper::Logger.new($stdout, :debug) -puts "mysql://#{ENV['db_user']}:#{ENV['db_pass']}@#{ENV['db_host']}/#{ENV['db_data']}" -DataMapper.setup(:default, "mysql://#{ENV['db_user']}:#{ENV['db_pass']}@#{ENV['db_host']}/#{ENV['db_data']}") - -class User +class User < BaseModel include DataMapper::Resource - - API_VERSION = 'v2' - BASE_URL = 'http://github.com/api/' + API_VERSION + '/json/user/show/' - + property :id, Serial, :lazy => false property :github_id, Text, :lazy => false property :gravatar_id, Text, :lazy => false property :login, Text, :lazy => false - property :email, Text, :lazy => false - property :name, Text, :lazy => false - property :blog, Text, :lazy => false + property :email, Text, :lazy => false + property :name, Text, :lazy => false + property :blog, Text, :lazy => false property :company, Text, :lazy => false property :location, Text, :lazy => false property :type, Text, :lazy => false @@ -31,10 +24,10 @@ class User property :following_count, Text, :lazy => false property :followers_count, Text, :lazy => false property :updated_at, DateTime, :lazy => false - + def self.create_from_username(username) - if found_user = User.first(:login => username) + if found_user = User.first(:login => username) if Time.now - Time.parse(found_user.updated_at.to_s) <= 60*60*24 puts "User created less than 24 hours ago. Returning DB record" return found_user @@ -46,17 +39,17 @@ def self.create_from_username(username) puts "User not found; using web services" user = User.new end - + user_data_url = User.get_user_data_url(username) user_data_response = get_json_response(user_data_url) user_data = JSON.parse(user_data_response.body) user_data = user_data['user'] - + user.github_id = user_data['id'] user.gravatar_id = user_data['gravatar_id'] user.login = user_data['login'] user.email = user_data['email'] - user.name = user_data['name'] + user.name = user_data['name'] user.blog = user_data['blog'] user.company = user_data['company'] user.location = user_data['location'] @@ -71,13 +64,13 @@ def self.create_from_username(username) user.save! return user end - + def self.get_json_response(url) Net::HTTP.get_response(URI.parse(url)) end def self.get_user_data_url(username) - return BASE_URL + username + return USER_BASE_URL + username end end diff --git a/lib/helpers.rb b/lib/helpers.rb new file mode 100644 index 0000000..07f57fc --- /dev/null +++ b/lib/helpers.rb @@ -0,0 +1,20 @@ +class BaseModel + API_VERSION = 'v2' + BASE_URL = 'http://github.com/api/' + API_VERSION + '/json/' + + USER_BASE_URL = BASE_URL + 'user/show/' + REPO_BASE_URL = BASE_URL + 'repos/show/' + COMMITS_BASE_URL = BASE_URL + 'commits/list/' +end + +class DateTime + def ical_timestamp + strftime("%Y%m%dT%H%M%SZ") + end +end + +class String + def remove_indent + self =~ /\A([ \t]+)/ ? gsub(/\n#{$1}/, "\n").strip : self + end +end diff --git a/views/ical.erb b/views/ical.erb new file mode 100644 index 0000000..5d7cd89 --- /dev/null +++ b/views/ical.erb @@ -0,0 +1,30 @@ +BEGIN:VCALENDAR +METHOD:PUBLISH +X-WR-TIMEZONE:Europe/Berlin +PRODID:-//Apple Inc.//iCal 3.0//EN +CALSCALE:GREGORIAN +X-WR-CALNAME:GitHub: <%= @repo.owner %>/<%= @repo.name %> +VERSION:2.0 +X-WR-RELCALID:<%= @repo.ical_uuid %> +X-APPLE-CALENDAR-COLOR:#0252D4 +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +DTSTART:19810329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +TZNAME:CEST +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +DTSTART:19961027T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +TZNAME:CET +END:STANDARD +END:VTIMEZONE +<% @commits.each do |commit| %> +<%= commit.to_ical %> +<% end %> +END:VCALENDAR diff --git a/views/repo.erb b/views/repo.erb index 59303ab..ac1bcef 100644 --- a/views/repo.erb +++ b/views/repo.erb @@ -5,22 +5,25 @@
ID
<%= @repo.id %>
- +
URL
<%= @repo.url %>
- +
Homepage
<%= @repo.homepage %>
- +
Name
<%= @repo.name %>
- +
Description
<%= @repo.description %>
- +
Created by
<%= @repo.owner %>
- + +
Commit Log
+
iCal: <%= request.url %>.ics
+
<%= erb :footer %>