toolmantim-bananajour 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,66 @@
1
+ require "rubygems"
2
+
3
+ $:.unshift "#{File.dirname(__FILE__)}/lib"
4
+
5
+ require "bananajour/version"
6
+ require "bananajour/gem_dependencies"
7
+
8
+ gem = Gem::Specification.new do |gem|
9
+ gem.name = "bananajour"
10
+ gem.version = Bananajour::VERSION
11
+ gem.platform = Gem::Platform::RUBY
12
+ gem.extra_rdoc_files = ["Readme.md"]
13
+ gem.summary = "Local git repository hosting with a sexy web interface and bonjour discovery. It's like your own little adhoc, network-aware github!"
14
+ gem.description = gem.summary
15
+ gem.authors = ["Tim Lucas"]
16
+ gem.email = "t.lucas@toolmantim.com"
17
+ gem.homepage = "http://github.com/toolmantim/bananajour"
18
+ gem.require_path = "lib"
19
+ gem.files = %w(Readme.md Rakefile) + Dir.glob("{bin,lib,sinatra}/**/*")
20
+ gem.has_rdoc = false
21
+ gem.bindir = 'bin'
22
+ gem.executables = [ 'bananajour' ]
23
+ Bananajour::GemDependencies.all.each {|dep| gem.add_runtime_dependency( dep.name, dep.version ) }
24
+ end
25
+
26
+ task :clean do
27
+ FileUtils.rm_rf Dir['*.gem', '*.gemspec']
28
+ end
29
+
30
+ namespace :gem do
31
+
32
+ desc "Rebuild and install bananajour as a gem"
33
+ task :install => :package do
34
+ require 'rubygems/installer'
35
+ Dir['*.gem'].each do |gem|
36
+ Gem::Installer.new(gem).install
37
+ end
38
+ end
39
+
40
+ desc "Create the gem"
41
+ task :package => [:clean, "spec:generate"] do
42
+ require 'rubygems/builder'
43
+ Gem::Builder.new( gem ).build
44
+ end
45
+
46
+ namespace :spec do
47
+
48
+ desc "Update #{gem.name}.gemspec"
49
+ task :generate do
50
+ File.open("#{gem.name}.gemspec", "w") do |f|
51
+ f.puts(gem.to_ruby)
52
+ end
53
+ end
54
+
55
+ desc "Test spec in github cleanroom"
56
+ task :test => :generate do
57
+ require 'rubygems/specification'
58
+ data = File.read("#{gem.name}.gemspec")
59
+ spec = nil
60
+ Thread.new { spec = eval("$SAFE = 3\n#{data}") }.join
61
+ puts "#{spec} - Good to go!"
62
+ end
63
+
64
+ end
65
+
66
+ end
data/Readme.md ADDED
@@ -0,0 +1,66 @@
1
+ Bananajour - Local git publication and collaboration
2
+ ====================================================
3
+
4
+ Local git repository hosting with a sexy web interface and Bonjour discovery. It's like a bunch of adhoc, local, network-aware githubs!
5
+
6
+ Unlike Gitjour, the repositories you're serving are not your working git repositories, they're served from `~/.bananajour/repositories`. You can push to your bananajour repositories from your working copies just like you do with github.
7
+
8
+ Bananajour is the baby of [Tim Lucas](http://toolmantim.com/) with much railscamp hackage by [Nathan de Vries](http://github.com/atnan), [Lachlan Hardy](http://github.com/lachlanhardy), [Josh Bassett](http://github.com/nullobject), [Myles Byrne](http://github.com/quackingduck), [Ben Hoskings](http://github.com/benhoskings), [Brett Goulder](http://github.com/brettgo1), [Tony Issakov](https://github.com/tissak), and [Mark Bennett](http://github.com/MarkBennett). The rad logo was by [Carla Hackett](http://carlahackettdesign.com/).
9
+
10
+ Installation and usage
11
+ ----------------------
12
+
13
+ You'll need at least [git version 1.6](http://git-scm.com/). Run `git --version` if you're unsure.
14
+
15
+ Install it from github via gems:
16
+
17
+ gem install toolmantim-bananajour
18
+
19
+ (you might need to do a `gem sources -a http://gems.github.com` beforehand!)
20
+
21
+ Start it up:
22
+
23
+ bananajour
24
+
25
+ Initialize a new Bananajour repository:
26
+
27
+ cd ~/code/myproj
28
+ bananajour init
29
+
30
+ Publish your codez:
31
+
32
+ git push banana master
33
+
34
+ Fire up [http://localhost:9331/](http://localhost:9331/) to check it out.
35
+
36
+ If somebody starts sharing a Bananajour repository with the same name on the
37
+ network, it'll automatically show up in the network thanks to the wonder that is Bonjour.
38
+
39
+ Official repository and support
40
+ -------------------------------
41
+
42
+ [http://github.com/toolmantim/bananajour](http://github.com/toolmantim/bananajour) is where Bananajour lives along with all of its support issues.
43
+
44
+ Developing
45
+ ----------
46
+
47
+ If you want to hack on the sinatra app then do so from a local clone but run your actual bananjour from the gem version. Running the sinatra app directly won't broadcast it onto the network and it'll run on a different port:
48
+
49
+ ruby sinatra/app.rb -s thin
50
+
51
+ If you want code reloading use [shotgun](http://github.com/rtomayko/shotgun) instead:
52
+
53
+ shotgun sinatra/app.rb -s thin
54
+
55
+ If you then want to run your working copy as your public bananajour rebuild and install it as a gem:
56
+
57
+ sudo rake gem:install
58
+
59
+ License
60
+ -------
61
+
62
+ All directories and files are MIT Licensed.
63
+
64
+ Warning to all those who still believe secrecy will save their revenue stream
65
+ -----------------------------------------------------------------------------
66
+ Bananas were meant to be shared. There are no secret bananas.
data/bin/bananajour ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "rubygems"
4
+ require "#{File.dirname(__FILE__)}/../lib/bananajour"
5
+
6
+ Thread.abort_on_exception = true
7
+
8
+ Bananajour.check_git!
9
+ Bananajour.check_git_config!
10
+
11
+ case ARGV.first
12
+
13
+ when nil
14
+ Bananajour.serve_web!
15
+ Bananajour.serve_git!
16
+ Bananajour.advertise!
17
+ Process.wait
18
+
19
+ when "init", "add"
20
+ repo = Bananajour.init!(ARGV[1] || File.expand_path("."))
21
+
22
+ when "remove", "rm"
23
+ name = ARGV[1]
24
+
25
+ if !name || name.empty?
26
+ abort "You need to specify the name of the repository you'd like to remove:\n#{File.basename($0)} remove <path>"
27
+ elsif !(repo = Bananajour::Repository.for_name(name))
28
+ abort "#{name.inspect} is not a valid repository name"
29
+ end
30
+
31
+ repo.remove!
32
+
33
+ when "clone"
34
+ if ARGV[1].nil? || ARGV[1].empty?
35
+ abort "You need to specify the path to the repository you'd like to clone:\n$ bananajour clone <path>"
36
+ end
37
+ repo = Bananajour.clone!(ARGV[1], ARGV[2])
38
+
39
+ when "help", "--help", "-h"
40
+ puts <<-HELP
41
+ Usage: #{File.basename($0)} [<command>]
42
+
43
+ Commands:
44
+ none - Start the web, git and bonjour serving
45
+ init [path] - Init a new repo
46
+ remove <name> - Remove a repo
47
+ clone <url> [path] - Clone a remote repo and init it as a new bananajour repo
48
+ help
49
+ version
50
+ HELP
51
+
52
+ when "version", "--version", "-v"
53
+ puts "bananajour version #{Bananajour::VERSION}"
54
+
55
+ else
56
+ abort "Say what? Try: #{File.basename($0)} help"
57
+ end
@@ -0,0 +1,55 @@
1
+ class Bananajour::Bonjour::Advertiser
2
+ def initialize
3
+ @services = []
4
+ end
5
+ def go!
6
+ register_app
7
+ register_repos
8
+ end
9
+ private
10
+ def register_app
11
+ STDERR.puts "Registering #{Bananajour.web_uri}"
12
+ tr = DNSSD::TextRecord.new
13
+ tr["name"] = Bananajour.config.name
14
+ tr["email"] = Bananajour.config.email
15
+ tr["uri"] = Bananajour.web_uri
16
+ tr["gravatar"] = Bananajour.gravatar
17
+ DNSSD.register("#{Bananajour.config.name}'s bananajour", "_http._tcp,_bananajour", nil, Bananajour.web_port, tr) {}
18
+ end
19
+ def register_repos
20
+ loop do
21
+ stop_old_services
22
+ register_new_repositories
23
+ sleep(1)
24
+ end
25
+ end
26
+ def stop_old_services
27
+ old_services.each do |old_service|
28
+ STDERR.puts "Unregistering #{old_service.repository.uri}"
29
+ old_service.stop
30
+ @services.delete(old_service)
31
+ end
32
+ end
33
+ def old_services
34
+ @services.reject {|s| Bananajour.repositories.include?(s.repository)}
35
+ end
36
+ def register_new_repositories
37
+ new_repositories.each do |new_repo|
38
+ STDERR.puts "Registering #{new_repo.uri}"
39
+ tr = DNSSD::TextRecord.new
40
+ tr["name"] = new_repo.name
41
+ tr["uri"] = new_repo.uri
42
+ tr["bjour-name"] = Bananajour.config.name
43
+ tr["bjour-email"] = Bananajour.config.email
44
+ tr["bjour-uri"] = Bananajour.web_uri
45
+ tr["bjour-gravatar"] = Bananajour.gravatar
46
+ service = DNSSD.register(new_repo.name, "_git._tcp,_bananajour", nil, 9418, tr) {}
47
+ service.class.instance_eval { attr_accessor(:repository) }
48
+ service.repository = new_repo
49
+ @services << service
50
+ end
51
+ end
52
+ def new_repositories
53
+ Bananajour.repositories.select {|repo| !@services.any? {|s| s.repository == repo } }
54
+ end
55
+ end
@@ -0,0 +1,24 @@
1
+ module Bananajour::Bonjour
2
+ class BananajourBrowser
3
+
4
+ def initialize
5
+ @browser = Browser.new('_bananajour._http._tcp')
6
+ end
7
+
8
+ def bananajours
9
+ @browser.replies.map do |reply|
10
+ Person.new(
11
+ reply.text_record["name"],
12
+ reply.text_record["email"],
13
+ reply.text_record["uri"],
14
+ reply.text_record["gravatar"]
15
+ )
16
+ end
17
+ end
18
+
19
+ def other_bananajours
20
+ bananajours.reject {|b| b.uri == Bananajour.web_uri}
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,52 @@
1
+ Bananajour.require_gem 'dnssd'
2
+
3
+ require 'thread'
4
+ require 'timeout'
5
+
6
+ Thread.abort_on_exception = true
7
+
8
+ # Generic bonjour browser
9
+ #
10
+ # Example use:
11
+ #
12
+ # browser = BonjourBrowser.new("_bananajour._git._tcp")
13
+ # loop do
14
+ # sleep(1)
15
+ # pp browser.replies.map {|r| r.name}
16
+ # end
17
+ #
18
+ # Probably gem-worthy
19
+ class Bananajour::Bonjour::Browser
20
+ attr_reader :replies
21
+ def initialize(service)
22
+ @service = service
23
+ @mutex = Mutex.new
24
+ @replies = []
25
+ watch!
26
+ end
27
+ private
28
+ def watch!
29
+ DNSSD.browse(@service) do |br|
30
+ begin
31
+ Timeout.timeout(5) do
32
+ DNSSD.resolve(br.name, br.type, br.domain) do |rr|
33
+ begin
34
+ @mutex.synchronize do
35
+ rr_exists = Proc.new {|existing_rr| existing_rr.target == rr.target && existing_rr.fullname == rr.fullname}
36
+ if (DNSSD::Flags::Add & br.flags.to_i) != 0
37
+ @replies << rr unless @replies.any?(&rr_exists)
38
+ else
39
+ @replies.delete_if(&rr_exists)
40
+ end
41
+ end
42
+ ensure
43
+ rr.service.stop
44
+ end
45
+ end
46
+ end
47
+ rescue Timeout::Error
48
+ # Do nothing
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,21 @@
1
+ class Bananajour::Bonjour::Person
2
+
3
+ attr_accessor :name, :email, :uri , :gravatar
4
+
5
+ def initialize(name, email, uri, gravatar)
6
+ @name, @email, @uri, @gravatar = name, email, uri, gravatar
7
+ end
8
+
9
+ def ==(other)
10
+ self.uri == other.uri
11
+ end
12
+
13
+ def hash
14
+ to_hash.hash
15
+ end
16
+
17
+ def to_hash
18
+ {"name" => name, "email" => email, "uri" => uri, "gravatar" => gravatar}
19
+ end
20
+
21
+ end
@@ -0,0 +1,39 @@
1
+ class Bananajour::Bonjour::Repository
2
+
3
+ attr_accessor :name, :uri, :person
4
+
5
+ def initialize(name, uri, person)
6
+ @name, @uri, @person = name, uri, person
7
+ end
8
+
9
+ def html_friendly_name
10
+ Bananajour::Repository.html_friendly_name(name)
11
+ end
12
+
13
+ def ==(other)
14
+ self.uri == other.uri
15
+ end
16
+
17
+ def hash
18
+ to_hash.hash
19
+ end
20
+
21
+ def json_uri
22
+ "#{person.uri}#{name}.json"
23
+ end
24
+
25
+ def web_uri
26
+ "#{person.uri}##{html_friendly_name}"
27
+ end
28
+
29
+ def to_hash
30
+ {
31
+ "name" => name,
32
+ "uri" => uri,
33
+ "json_uri" => json_uri,
34
+ "web_uri" => web_uri,
35
+ "person" => person.to_hash
36
+ }
37
+ end
38
+
39
+ end
@@ -0,0 +1,36 @@
1
+ module Bananajour::Bonjour
2
+ class RepositoryBrowser
3
+
4
+ def initialize
5
+ @browser = Browser.new('_bananajour._git._tcp')
6
+ end
7
+
8
+ def repositories
9
+ @browser.replies.map do |reply|
10
+ Repository.new(
11
+ reply.text_record["name"],
12
+ reply.text_record["uri"],
13
+ Person.new(
14
+ reply.text_record["bjour-name"],
15
+ reply.text_record["bjour-email"],
16
+ reply.text_record["bjour-uri"],
17
+ reply.text_record["bjour-gravatar"]
18
+ )
19
+ )
20
+ end
21
+ end
22
+
23
+ def other_repositories
24
+ repositories.reject {|r| Bananajour.repositories.any? {|my_rep| my_rep.name == r.name}}
25
+ end
26
+
27
+ def repositories_similar_to(repository)
28
+ repositories.select {|r| r.name == repository.name}
29
+ end
30
+
31
+ def repositories_for(person)
32
+ repositories.select {|r| r.person == person}
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,9 @@
1
+ module Bananajour::Bonjour
2
+ end
3
+
4
+ require 'bananajour/bonjour/person'
5
+ require 'bananajour/bonjour/repository'
6
+ require 'bananajour/bonjour/browser'
7
+ require 'bananajour/bonjour/repository_browser'
8
+ require 'bananajour/bonjour/bananajour_browser'
9
+ require 'bananajour/bonjour/advertiser'
@@ -0,0 +1,96 @@
1
+ module Bananajour::Commands
2
+
3
+ def check_git!
4
+ if (version = `git --version`.strip) =~ /git version 1\.[12345]/
5
+ abort "You have #{version}, you need at least 1.6"
6
+ end
7
+ end
8
+
9
+ def check_git_config!
10
+ config_message = lambda {|key, example| "You haven't set your #{key} in your git config yet. To set it: git config --global #{key} '#{example}'"}
11
+ abort(config_message["user.name", "My Name"]) if config.name.empty?
12
+ abort(config_message["user.email", "name@domain.com"]) if config.email.empty?
13
+ end
14
+
15
+ def serve_web!
16
+ if repositories.empty?
17
+ STDERR.puts "Warning: you don't have any bananajour repositories. Use: bananajour init"
18
+ end
19
+ fork { exec "/usr/bin/env ruby #{File.dirname(__FILE__)}/../../sinatra/app.rb -p #{web_port} -e production" }
20
+ puts "* Started " + web_uri.foreground(:yellow)
21
+ end
22
+
23
+ def serve_git!
24
+ fork { exec "git daemon --base-path=#{repositories_path} --export-all" }
25
+ puts "* Started " + "#{git_uri}".foreground(:yellow)
26
+ end
27
+
28
+ def serve_git!
29
+ fork { exec "git daemon --base-path=#{repositories_path} --export-all" }
30
+ puts "* Started " + "#{git_uri}".foreground(:yellow)
31
+ end
32
+
33
+ def advertise!
34
+ fork { Bananajour::Bonjour::Advertiser.new.go! }
35
+ end
36
+
37
+ def init!(dir, name = nil)
38
+ dir = Fancypath(dir)
39
+
40
+ unless dir.join(".git").directory?
41
+ abort "Can't init project #{dir}, no .git directory found."
42
+ end
43
+
44
+ if name.nil?
45
+ default_name = dir.basename.to_s
46
+ print "Project Name?".foreground(:yellow) + " [#{default_name}] "
47
+ name = (STDIN.gets || "").strip
48
+ name = default_name if name.empty?
49
+ end
50
+
51
+ repo = Bananajour::Repository.for_name(name)
52
+
53
+ if repo.exists?
54
+ abort "You've already a project #{repo}."
55
+ end
56
+
57
+ repo.init!
58
+ Dir.chdir(dir) { `git remote add banana #{repo.path.expand_path}` }
59
+ puts init_success_message(repo.dirname)
60
+
61
+ repo
62
+ end
63
+
64
+ def init_success_message(repo_dirname)
65
+ plain_init_success_message(repo_dirname).gsub("git push banana master", "git push banana master".foreground(:yellow))
66
+ end
67
+
68
+ def plain_init_success_message(repo_dirname)
69
+ "Bananajour repository #{repo_dirname} initialised and remote banana added.\nNext: git push banana master"
70
+ end
71
+
72
+ def clone!(url, clone_name)
73
+ dir = clone_name || File.basename(url).chomp('.git')
74
+
75
+ if File.exists?(dir)
76
+ abort "Can't clone #{url} to #{dir}, the directory already exists."
77
+ end
78
+
79
+ `git clone #{url} #{dir}`
80
+ if $? != 0
81
+ abort clone_failure_message(url, repo.dirname)
82
+ else
83
+ puts clone_success_message(url, dir)
84
+ init!(dir, dir)
85
+ end
86
+ end
87
+
88
+ def clone_success_message(source_repo_url, repo_dirname)
89
+ "Bananajour repository #{source_repo_url} cloned to #{repo_dirname}."
90
+ end
91
+
92
+ def clone_failure_message(source_repo_url, repo_dirname)
93
+ "Failed to clone Bananajour repository #{source_repo_url} to #{repo_dirname}."
94
+ end
95
+
96
+ end
@@ -0,0 +1,34 @@
1
+ module Bananajour
2
+ # DRYs version number dependencies and provides a simple way require them
3
+ module GemDependencies
4
+ DEPENDENCIES = [
5
+ %w( sinatra 0.9.2 ),
6
+ %w( json 1.1.2 ),
7
+ %w( chrislloyd-fancypath 0.5.8 ),
8
+ %w( rainbow 1.0.1 ),
9
+ %w( mojombo-grit 1.1.1 ),
10
+ %w( dnssd 0.7.1 ),
11
+ %w( rack 1.0.0 ),
12
+ %w( thin 1.0.0 ),
13
+ %w( haml 2.0.9 ),
14
+ %w( activesupport 2.3.2 )
15
+ ]
16
+ class Dependency < Struct.new(:name, :version)
17
+ def require_gem; gem name, version end
18
+ end
19
+ def self.all
20
+ DEPENDENCIES.map {|(name, version)| Dependency.new(name, version)}
21
+ end
22
+ def self.for_name(name)
23
+ all.find {|d| d.name == name }
24
+ end
25
+ end
26
+
27
+ def self.gem(name)
28
+ Bananajour::GemDependencies.for_name(name).require_gem
29
+ end
30
+ def self.require_gem(name, lib=nil)
31
+ self.gem(name)
32
+ Kernel.require(lib || name)
33
+ end
34
+ end
@@ -0,0 +1,13 @@
1
+ require 'md5'
2
+
3
+ Grit::Commit.class_eval do
4
+ def ==(other)
5
+ self.id == other.id
6
+ end
7
+ end
8
+
9
+ Grit::Actor.class_eval do
10
+ def gravatar_uri
11
+ Bananajour.gravatar_uri(email)
12
+ end
13
+ end
@@ -0,0 +1,98 @@
1
+ module Bananajour
2
+ module GravatarHelpers
3
+ def gravatar
4
+ gravatar_uri(self.config.email)
5
+ end
6
+ def gravatar_uri(email)
7
+ "http://gravatar.com/avatar/#{MD5.md5(email)}.png"
8
+ end
9
+ end
10
+
11
+ # Lifted from Rails
12
+ module DateHelpers
13
+ # Reports the approximate distance in time between two Time or Date objects or integers as seconds.
14
+ # Set <tt>include_seconds</tt> to true if you want more detailed approximations when distance < 1 min, 29 secs
15
+ # Distances are reported based on the following table:
16
+ #
17
+ # 0 <-> 29 secs # => less than a minute
18
+ # 30 secs <-> 1 min, 29 secs # => 1 minute
19
+ # 1 min, 30 secs <-> 44 mins, 29 secs # => [2..44] minutes
20
+ # 44 mins, 30 secs <-> 89 mins, 29 secs # => about 1 hour
21
+ # 89 mins, 29 secs <-> 23 hrs, 59 mins, 29 secs # => about [2..24] hours
22
+ # 23 hrs, 59 mins, 29 secs <-> 47 hrs, 59 mins, 29 secs # => 1 day
23
+ # 47 hrs, 59 mins, 29 secs <-> 29 days, 23 hrs, 59 mins, 29 secs # => [2..29] days
24
+ # 29 days, 23 hrs, 59 mins, 30 secs <-> 59 days, 23 hrs, 59 mins, 29 secs # => about 1 month
25
+ # 59 days, 23 hrs, 59 mins, 30 secs <-> 1 yr minus 1 sec # => [2..12] months
26
+ # 1 yr <-> 2 yrs minus 1 secs # => about 1 year
27
+ # 2 yrs <-> max time or date # => over [2..X] years
28
+ #
29
+ # With <tt>include_seconds</tt> = true and the difference < 1 minute 29 seconds:
30
+ # 0-4 secs # => less than 5 seconds
31
+ # 5-9 secs # => less than 10 seconds
32
+ # 10-19 secs # => less than 20 seconds
33
+ # 20-39 secs # => half a minute
34
+ # 40-59 secs # => less than a minute
35
+ # 60-89 secs # => 1 minute
36
+ #
37
+ # ==== Examples
38
+ # from_time = Time.now
39
+ # distance_of_time_in_words(from_time, from_time + 50.minutes) # => about 1 hour
40
+ # distance_of_time_in_words(from_time, 50.minutes.from_now) # => about 1 hour
41
+ # distance_of_time_in_words(from_time, from_time + 15.seconds) # => less than a minute
42
+ # distance_of_time_in_words(from_time, from_time + 15.seconds, true) # => less than 20 seconds
43
+ # distance_of_time_in_words(from_time, 3.years.from_now) # => over 3 years
44
+ # distance_of_time_in_words(from_time, from_time + 60.hours) # => about 3 days
45
+ # distance_of_time_in_words(from_time, from_time + 45.seconds, true) # => less than a minute
46
+ # distance_of_time_in_words(from_time, from_time - 45.seconds, true) # => less than a minute
47
+ # distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute
48
+ # distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year
49
+ # distance_of_time_in_words(from_time, from_time + 4.years + 15.days + 30.minutes + 5.seconds) # => over 4 years
50
+ #
51
+ # to_time = Time.now + 6.years + 19.days
52
+ # distance_of_time_in_words(from_time, to_time, true) # => over 6 years
53
+ # distance_of_time_in_words(to_time, from_time, true) # => over 6 years
54
+ # distance_of_time_in_words(Time.now, Time.now) # => less than a minute
55
+ #
56
+ def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false)
57
+ from_time = from_time.to_time if from_time.respond_to?(:to_time)
58
+ to_time = to_time.to_time if to_time.respond_to?(:to_time)
59
+ distance_in_minutes = (((to_time - from_time).abs)/60).round
60
+ distance_in_seconds = ((to_time - from_time).abs).round
61
+
62
+ case distance_in_minutes
63
+ when 0..1
64
+ return (distance_in_minutes == 0) ? 'less than a minute' : '1 minute' unless include_seconds
65
+ case distance_in_seconds
66
+ when 0..4 then 'less than 5 seconds'
67
+ when 5..9 then 'less than 10 seconds'
68
+ when 10..19 then 'less than 20 seconds'
69
+ when 20..39 then 'half a minute'
70
+ when 40..59 then 'less than a minute'
71
+ else '1 minute'
72
+ end
73
+
74
+ when 2..44 then "#{distance_in_minutes} minutes"
75
+ when 45..89 then 'about 1 hour'
76
+ when 90..1439 then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
77
+ when 1440..2879 then '1 day'
78
+ when 2880..43199 then "#{(distance_in_minutes / 1440).round} days"
79
+ when 43200..86399 then 'about 1 month'
80
+ when 86400..525599 then "#{(distance_in_minutes / 43200).round} months"
81
+ when 525600..1051199 then 'about 1 year'
82
+ else "over #{(distance_in_minutes / 525600).round} years"
83
+ end
84
+ end
85
+
86
+ # Like distance_of_time_in_words, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>.
87
+ #
88
+ # ==== Examples
89
+ # time_ago_in_words(3.minutes.from_now) # => 3 minutes
90
+ # time_ago_in_words(Time.now - 15.hours) # => 15 hours
91
+ # time_ago_in_words(Time.now) # => less than a minute
92
+ #
93
+ # from_time = Time.now - 3.days - 14.minutes - 25.seconds # => 3 days
94
+ def time_ago_in_words(from_time, include_seconds = false)
95
+ distance_of_time_in_words(from_time, Time.now, include_seconds)
96
+ end
97
+ end
98
+ end