toolmantim-bananajour 2.1.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.
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