tomatoharvest 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +32 -0
- data/Rakefile +2 -0
- data/TODO +4 -0
- data/bin/toma +5 -0
- data/lib/tomatoharvest.rb +11 -0
- data/lib/tomatoharvest/cli.rb +46 -0
- data/lib/tomatoharvest/config.rb +21 -0
- data/lib/tomatoharvest/list.rb +76 -0
- data/lib/tomatoharvest/notifier.rb +15 -0
- data/lib/tomatoharvest/notifier/notification_center.rb +14 -0
- data/lib/tomatoharvest/os.rb +12 -0
- data/lib/tomatoharvest/pomodoro.rb +10 -0
- data/lib/tomatoharvest/task.rb +26 -0
- data/lib/tomatoharvest/time_entry.rb +110 -0
- data/lib/tomatoharvest/timer.rb +61 -0
- data/lib/tomatoharvest/tmux.rb +32 -0
- data/lib/tomatoharvest/version.rb +3 -0
- data/spec/helper.rb +81 -0
- data/spec/lib/tomatoharvest/cli_spec.rb +66 -0
- data/spec/lib/tomatoharvest/config_spec.rb +44 -0
- data/spec/lib/tomatoharvest/list_spec.rb +51 -0
- data/spec/lib/tomatoharvest/time_entry_spec.rb +106 -0
- data/spec/lib/tomatoharvest/timer_spec.rb +47 -0
- data/tomatoharvest.gemspec +30 -0
- metadata +191 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f508e0466cc97b1f3b0d619bdfacbd2344b8cb8f
|
4
|
+
data.tar.gz: 3284680bde183e758259ec764f3ed84209931501
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5544110fda87a0bb5b50d50f3a4e3d6e2da532088317ba286c6f4571950f33811d7ff03455a9ed2b2fc2668ca8d65a4eec134e70a39c0dd3e9f1a49e7295d418
|
7
|
+
data.tar.gz: 7786c551b13055b700232d439cfe52f5c890199579e5b60f7e6aac41bdb0732b73c2116e11a74d9d30f060e651dc04346d3f93bf20ac5ac0047d0d16293c9beb
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Sam Reh
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# TomatoHarvest
|
2
|
+
Command line pomodoro timer that logs to Harvest.
|
3
|
+
|
4
|
+
## Installation
|
5
|
+
|
6
|
+
$ gem install tomatoharvest
|
7
|
+
|
8
|
+
Create a file called ~/.tomaconfig with options:
|
9
|
+
```yaml
|
10
|
+
domain: myharvestdomain
|
11
|
+
username: username
|
12
|
+
password: password
|
13
|
+
project: harvest project
|
14
|
+
task: harvest task
|
15
|
+
```
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
$ toma add "Some Task I Have To Do"
|
20
|
+
$ toma list
|
21
|
+
$ toma start 1
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it ( https://github.com/samuelreh/tomatoharvest/fork )
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create a new Pull Request
|
30
|
+
|
31
|
+
## Credits
|
32
|
+
This application is heavily inspired by https://github.com/visionmedia/pomo.
|
data/Rakefile
ADDED
data/bin/toma
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'tomatoharvest/version'
|
2
|
+
require 'tomatoharvest/list'
|
3
|
+
require 'tomatoharvest/task'
|
4
|
+
require 'tomatoharvest/timer'
|
5
|
+
require 'tomatoharvest/cli'
|
6
|
+
require 'tomatoharvest/os'
|
7
|
+
require 'tomatoharvest/notifier'
|
8
|
+
require 'tomatoharvest/config'
|
9
|
+
require 'tomatoharvest/time_entry'
|
10
|
+
require 'tomatoharvest/pomodoro'
|
11
|
+
require 'tomatoharvest/tmux'
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module TomatoHarvest
|
4
|
+
class CLI < ::Thor
|
5
|
+
DEFAULT_MINUTES = 25
|
6
|
+
|
7
|
+
desc "add", "add a task"
|
8
|
+
def add(name)
|
9
|
+
task = Task.new(name)
|
10
|
+
List.add(task)
|
11
|
+
say "#{task.name} added with id #{task.id}"
|
12
|
+
end
|
13
|
+
|
14
|
+
desc "list", "list all tasks"
|
15
|
+
def list
|
16
|
+
list = List.all.map do |task|
|
17
|
+
[task.id, task.name]
|
18
|
+
end
|
19
|
+
list.unshift(['id', 'name'])
|
20
|
+
|
21
|
+
shell = Thor::Base.shell.new
|
22
|
+
shell.print_table(list)
|
23
|
+
end
|
24
|
+
|
25
|
+
desc "start", "start a task"
|
26
|
+
def start(id, minutes = DEFAULT_MINUTES)
|
27
|
+
task = List.find(id)
|
28
|
+
config = Config.load.merge("name" => task.name)
|
29
|
+
entry = TimeEntry.build_and_test(config)
|
30
|
+
|
31
|
+
say "Timer started for #{task.name}"
|
32
|
+
Timer.start(task.id, minutes: minutes, time_entry: entry)
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "stop", "stop current timer"
|
36
|
+
def stop
|
37
|
+
if Timer.stop
|
38
|
+
say "Timer stopped"
|
39
|
+
else
|
40
|
+
say "Timer not running"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module TomatoHarvest
|
2
|
+
class Config
|
3
|
+
CONFIG_PATH = File.expand_path("#{ENV['$HOME']}/.tomaconfig")
|
4
|
+
LOCAL_CONFIG_PATH = File.join(Dir.pwd, '.tomaconfig')
|
5
|
+
|
6
|
+
def self.load(options = {})
|
7
|
+
if !(File.exists? CONFIG_PATH)
|
8
|
+
File.open(CONFIG_PATH, 'w') do |file|
|
9
|
+
YAML.dump({}, file)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
hash = YAML.load_file(CONFIG_PATH)
|
14
|
+
if File.exists? LOCAL_CONFIG_PATH
|
15
|
+
hash.merge!(YAML.load_file(LOCAL_CONFIG_PATH))
|
16
|
+
end
|
17
|
+
|
18
|
+
hash
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module TomatoHarvest
|
4
|
+
class List
|
5
|
+
PATH = File.expand_path("#{ENV['$HOME']}/.toma")
|
6
|
+
|
7
|
+
attr_reader :items
|
8
|
+
|
9
|
+
alias :all :items
|
10
|
+
|
11
|
+
def self.add(item)
|
12
|
+
new.tap do |list|
|
13
|
+
list.add(item)
|
14
|
+
list.save
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.all
|
19
|
+
new.all
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.find(id)
|
23
|
+
new.find(id)
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
if File.exists?(PATH)
|
28
|
+
@items = load_list
|
29
|
+
else
|
30
|
+
@items = []
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def find(id)
|
35
|
+
# TODO speed this up with an algo
|
36
|
+
all.find do |item|
|
37
|
+
item.id == id.to_i
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def save
|
42
|
+
yaml = YAML::dump(@items)
|
43
|
+
File.open(PATH, "w+") do |f|
|
44
|
+
f.write(yaml)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def add(item)
|
49
|
+
if last_item = @items.last
|
50
|
+
id = last_item.id
|
51
|
+
else
|
52
|
+
id = 0
|
53
|
+
end
|
54
|
+
item.id = id + 1
|
55
|
+
|
56
|
+
@items << item
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def load_list
|
62
|
+
string = ""
|
63
|
+
|
64
|
+
# better way to do this?
|
65
|
+
File.open(PATH, "r") do |f|
|
66
|
+
while line = f.gets
|
67
|
+
string += line
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
YAML::load(string)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative 'notifier/notification_center'
|
2
|
+
|
3
|
+
module TomatoHarvest
|
4
|
+
class Notifier
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@notifier = TomatoHarvest::Notifier::NotificationCenter.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def notify(message, opts = {})
|
11
|
+
@notifier.notify(message, opts)
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'terminal-notifier' if TomatoHarvest::OS.mac?
|
2
|
+
|
3
|
+
module TomatoHarvest
|
4
|
+
class Notifier
|
5
|
+
class NotificationCenter
|
6
|
+
def notify(message, opts = {})
|
7
|
+
title = 'TomatoHarvest'
|
8
|
+
|
9
|
+
TerminalNotifier.notify message, :title => title, :subtitle => opts[:subtitle]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module TomatoHarvest
|
2
|
+
class Task
|
3
|
+
|
4
|
+
attr_reader :name
|
5
|
+
attr_accessor :id, :pomodoros
|
6
|
+
|
7
|
+
def initialize(name)
|
8
|
+
@name = name
|
9
|
+
@pomodoros = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def log_pomodoro(seconds)
|
13
|
+
finished_at = DateTime.now
|
14
|
+
self.pomodoros << Pomodoro.new(seconds, finished_at)
|
15
|
+
end
|
16
|
+
|
17
|
+
def logged_minutes
|
18
|
+
sum = pomodoros.inject(0) do |sum, pomodoro|
|
19
|
+
sum + pomodoro.seconds
|
20
|
+
end
|
21
|
+
|
22
|
+
sum / 60.0
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'harvested'
|
3
|
+
|
4
|
+
module TomatoHarvest
|
5
|
+
class TimeEntry
|
6
|
+
|
7
|
+
attr_accessor :options
|
8
|
+
|
9
|
+
def self.build_and_test(options = {})
|
10
|
+
required = ["domain", "username", "password", "project", "task", "name"].to_set
|
11
|
+
keys = options.keys.to_set
|
12
|
+
if required.subset?(keys)
|
13
|
+
new(options).tap do |entry|
|
14
|
+
entry.test
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(options = {})
|
20
|
+
self.options = options
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Check that the project and task were found on Harvest
|
25
|
+
|
26
|
+
def test
|
27
|
+
raise "Couldn't find project" unless project
|
28
|
+
raise "Couldn't find task type" unless task
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Persist time entry to Harvest tracker
|
33
|
+
|
34
|
+
def log(seconds)
|
35
|
+
hours = seconds_to_hours(seconds)
|
36
|
+
options = {
|
37
|
+
notes: notes,
|
38
|
+
hours: hours,
|
39
|
+
spent_at: date,
|
40
|
+
project_id: project.id,
|
41
|
+
task_id: task.id
|
42
|
+
}
|
43
|
+
|
44
|
+
|
45
|
+
existing_entry = time_api.all.find do |entry|
|
46
|
+
entry.notes == self.notes
|
47
|
+
end
|
48
|
+
|
49
|
+
if existing_entry
|
50
|
+
existing_entry.hours += hours
|
51
|
+
existing_entry.spent_at = date
|
52
|
+
time_api.update(existing_entry)
|
53
|
+
else
|
54
|
+
entry = Harvest::TimeEntry.new(options)
|
55
|
+
time_api.create(entry)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Notes to send to tracker
|
61
|
+
|
62
|
+
def notes
|
63
|
+
options["name"]
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# Date for task (today), formatted properly for tracker
|
68
|
+
|
69
|
+
def date
|
70
|
+
Date.today
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Convert seconds into hours
|
75
|
+
|
76
|
+
def seconds_to_hours(seconds)
|
77
|
+
minutes = (seconds / 60.0)
|
78
|
+
unrounded = (minutes / 60.0)
|
79
|
+
(unrounded * 100).ceil / 100.0
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# Harvest project with name matching options[:project]
|
84
|
+
|
85
|
+
def project
|
86
|
+
time_api.trackable_projects.find do |project|
|
87
|
+
project.name == options["project"]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
##
|
92
|
+
# Harvest task with name matching options[:task]
|
93
|
+
|
94
|
+
def task
|
95
|
+
project.tasks.find do |task|
|
96
|
+
task.name == options["task"]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def time_api
|
103
|
+
@time_api ||= begin
|
104
|
+
client = Harvest.client(options["domain"], options["username"], options["password"])
|
105
|
+
Harvest::API::Time.new(client.credentials)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'daemons'
|
2
|
+
|
3
|
+
module TomatoHarvest
|
4
|
+
class Timer
|
5
|
+
PID_DIR = '~'
|
6
|
+
PID_NAME = '.toma'
|
7
|
+
|
8
|
+
def self.start(task_id, options = {})
|
9
|
+
new(task_id, options).start
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.stop
|
13
|
+
if monitor = Daemons::Monitor.find(File.expand_path(DIR), APP_NAME)
|
14
|
+
monitor.stop
|
15
|
+
true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(task_id, options = {})
|
20
|
+
@minutes = options[:minutes]
|
21
|
+
@time_entry = options[:time_entry]
|
22
|
+
@notifier = Notifier.new
|
23
|
+
@list = List.new
|
24
|
+
@task = @list.find(task_id)
|
25
|
+
@timer = 0
|
26
|
+
@tmux = Tmux.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def start
|
30
|
+
if Daemons.daemonize(app_name: PID_NAME, dir: PID_DIR, dir_mode: :normal)
|
31
|
+
at_exit { save_and_log }
|
32
|
+
run_timer
|
33
|
+
else
|
34
|
+
run_timer
|
35
|
+
save_and_log
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def run_timer
|
42
|
+
@notifier.notify "Pomodoro started for #{@minutes} minutes", :subtitle => @task.name
|
43
|
+
|
44
|
+
(@minutes * 60).times do |i|
|
45
|
+
sleep 1
|
46
|
+
@timer += 1
|
47
|
+
@tmux.update(@timer)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def save_and_log
|
52
|
+
@task.log_pomodoro(@timer)
|
53
|
+
@list.save
|
54
|
+
@time_entry.log(@timer) if @time_entry
|
55
|
+
@notifier.notify "Pomodoro finished", :subtitle => "Pomodoro finished!"
|
56
|
+
@tmux.update(0)
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module TomatoHarvest
|
2
|
+
class Tmux
|
3
|
+
|
4
|
+
def update(time)
|
5
|
+
write_tmux_time time
|
6
|
+
refresh_tmux_status_bar
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def tmux_time(time)
|
12
|
+
mm, ss = time.divmod(60)
|
13
|
+
ss = ss.to_s.rjust(2, "0")
|
14
|
+
"#[default]#[fg=green]#{mm}:#{ss}#[default]"
|
15
|
+
end
|
16
|
+
|
17
|
+
def write_tmux_time(time)
|
18
|
+
path = File.join(ENV['HOME'],'.tomatmux')
|
19
|
+
File.open(path, 'w') do |file|
|
20
|
+
file.write tmux_time(time)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def refresh_tmux_status_bar
|
25
|
+
pid = Process.fork do
|
26
|
+
exec "tmux refresh-client -S -t $(tmux list-clients -F '\#{client_tty}')"
|
27
|
+
end
|
28
|
+
Process.detach(pid)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
data/spec/helper.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
|
2
|
+
require 'thor'
|
3
|
+
require 'tomatoharvest'
|
4
|
+
|
5
|
+
require 'webmock/rspec'
|
6
|
+
require 'minitest/unit'
|
7
|
+
|
8
|
+
WebMock.disable_net_connect!(allow_localhost: true)
|
9
|
+
|
10
|
+
RSpec.configure do |c|
|
11
|
+
c.include MiniTest::Assertions
|
12
|
+
|
13
|
+
#
|
14
|
+
# Speed up the timer
|
15
|
+
#
|
16
|
+
c.before :all do
|
17
|
+
class TomatoHarvest::Timer
|
18
|
+
def sleep(time)
|
19
|
+
super(time/100000)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# Stub HTTP requests
|
26
|
+
#
|
27
|
+
c.before do
|
28
|
+
body = {
|
29
|
+
projects: [ {
|
30
|
+
name: 'Pomdoro',
|
31
|
+
id: 1,
|
32
|
+
tasks: [
|
33
|
+
{
|
34
|
+
name: 'Ruby Development',
|
35
|
+
id: 1
|
36
|
+
}
|
37
|
+
]
|
38
|
+
} ],
|
39
|
+
day_entries: []
|
40
|
+
}
|
41
|
+
|
42
|
+
stub_request(:get, /https:\/\/user:password@domain.harvestapp.com\/daily\/.*/).
|
43
|
+
with(:headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json; charset=utf-8', 'User-Agent'=>'Harvestable/2.0.0'}).
|
44
|
+
to_return(:status => 200, :body => body.to_json, :headers => {})
|
45
|
+
|
46
|
+
stub_request(:post, "https://user:password@domain.harvestapp.com/daily/add").
|
47
|
+
with(:headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json; charset=utf-8', 'User-Agent'=>'Harvestable/2.0.0'}).
|
48
|
+
to_return(:status => 200, :body => "", :headers => {})
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Don't daemonize for tests
|
53
|
+
# Dont notify the terminal
|
54
|
+
#
|
55
|
+
c.before do
|
56
|
+
Daemons.stub(daemonize: false)
|
57
|
+
TerminalNotifier.stub(notify: true)
|
58
|
+
TomatoHarvest::Tmux.any_instance.stub(update: true)
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# Cleanup .toma and .tomaconfig
|
63
|
+
#
|
64
|
+
|
65
|
+
[
|
66
|
+
["TomatoHarvest::Config::CONFIG_PATH", File.expand_path('spec/.tomaconfig')],
|
67
|
+
["TomatoHarvest::Config::LOCAL_CONFIG_PATH", File.expand_path('.tomaconfig')],
|
68
|
+
["TomatoHarvest::List::PATH", File.expand_path('spec/.toma')]
|
69
|
+
].each do |tuple|
|
70
|
+
path = tuple[1]
|
71
|
+
|
72
|
+
c.before :each do
|
73
|
+
stub_const(tuple[0], path)
|
74
|
+
File.delete(path) if File.exists?(path)
|
75
|
+
end
|
76
|
+
|
77
|
+
c.after :each do
|
78
|
+
File.delete(path) if File.exists?(path)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe TomatoHarvest::CLI do
|
4
|
+
|
5
|
+
describe 'add' do
|
6
|
+
|
7
|
+
it 'adds a task to the list' do
|
8
|
+
out = capture_io { TomatoHarvest::CLI.start ['add', 'foo'] }.join ''
|
9
|
+
expect(out).to match(/foo added with id 1/)
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
describe 'list' do
|
15
|
+
|
16
|
+
it 'shows a table of the tasks' do
|
17
|
+
TomatoHarvest::CLI.start ['add', 'foo']
|
18
|
+
TomatoHarvest::CLI.start ['add', 'bar']
|
19
|
+
out = capture_io { TomatoHarvest::CLI.start ['list'] }.join ''
|
20
|
+
expect(out).to match(/id name\n\s*1 foo\n\s*2 bar/)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'start' do
|
26
|
+
before do
|
27
|
+
TomatoHarvest::CLI.start ['add', 'foo']
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'starts the timer' do
|
31
|
+
out = capture_io { TomatoHarvest::CLI.start ['start', 1] }.join ''
|
32
|
+
expect(out).to match(/Timer started for foo/)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'starts the timer with specified length' do
|
36
|
+
out = capture_io { TomatoHarvest::CLI.start ['start', 1, 15] }.join ''
|
37
|
+
expect(out).to match(/Timer started for foo/)
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'when config has valid harvest options' do
|
41
|
+
|
42
|
+
before do
|
43
|
+
options = {
|
44
|
+
project: 'Pomodoro',
|
45
|
+
type: 'Ruby Development',
|
46
|
+
domain: 'domain',
|
47
|
+
username: 'user',
|
48
|
+
password: 'password'
|
49
|
+
}
|
50
|
+
|
51
|
+
path = TomatoHarvest::Config::CONFIG_PATH
|
52
|
+
File.open(path, 'w') do |file|
|
53
|
+
YAML::dump(options, file)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'starts the timer with specified length' do
|
58
|
+
out = capture_io { TomatoHarvest::CLI.start ['start', 1] }.join ''
|
59
|
+
expect(out).to match(/Timer started for foo/)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe TomatoHarvest::Config do
|
4
|
+
|
5
|
+
describe '.load' do
|
6
|
+
let(:global_options) do
|
7
|
+
{
|
8
|
+
project: 'Project',
|
9
|
+
type: 'Ruby Development',
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
before do
|
14
|
+
File.open(TomatoHarvest::Config::CONFIG_PATH, 'w') do |file|
|
15
|
+
YAML::dump(global_options, file)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'loads from the yaml config file' do
|
20
|
+
expect(TomatoHarvest::Config.load).to eql(global_options)
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'when there is a config file in the current dir' do
|
24
|
+
|
25
|
+
it 'overrides global config' do
|
26
|
+
options = {
|
27
|
+
type: 'JS Development',
|
28
|
+
}
|
29
|
+
local_config = File.join(Dir.pwd, '.tomaconfig')
|
30
|
+
|
31
|
+
File.open(local_config, 'w') do |file|
|
32
|
+
YAML::dump(options, file)
|
33
|
+
end
|
34
|
+
|
35
|
+
result = global_options.merge(options)
|
36
|
+
|
37
|
+
expect(TomatoHarvest::Config.load).to eql(result)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe TomatoHarvest::List do
|
4
|
+
|
5
|
+
def add_task(name)
|
6
|
+
task = TomatoHarvest::Task.new(name)
|
7
|
+
TomatoHarvest::List.add(task)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '.add' do
|
11
|
+
|
12
|
+
it 'adds to the list' do
|
13
|
+
add_task('foo')
|
14
|
+
expect(described_class.all.first).to be_an_instance_of(TomatoHarvest::Task)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '.list' do
|
20
|
+
|
21
|
+
it 'should have two items' do
|
22
|
+
add_task('foo')
|
23
|
+
add_task('bar')
|
24
|
+
expect(described_class.all.count).to eql(2)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '.find' do
|
30
|
+
|
31
|
+
it 'returns the task with the corresponding id' do
|
32
|
+
add_task('foo')
|
33
|
+
add_task('bar')
|
34
|
+
expect(described_class.find(1).name).to eql('foo')
|
35
|
+
expect(described_class.find(2).name).to eql('bar')
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#add' do
|
41
|
+
|
42
|
+
it 'adds the task to the items array' do
|
43
|
+
task = TomatoHarvest::Task.new('foo')
|
44
|
+
list = described_class.new
|
45
|
+
list.add(task)
|
46
|
+
expect(list.items.first.id).to eql(1)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe TomatoHarvest::TimeEntry do
|
4
|
+
|
5
|
+
describe '#test' do
|
6
|
+
|
7
|
+
let(:entry) do
|
8
|
+
described_class.new.tap do |entry|
|
9
|
+
entry.stub(project: double, task: double)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
it "raises an error if project can't be found" do
|
14
|
+
entry.stub(project: nil)
|
15
|
+
expect{ entry.test }.to raise_error("Couldn't find project")
|
16
|
+
end
|
17
|
+
|
18
|
+
it "raises an error if task can't be found" do
|
19
|
+
entry.stub(task: nil)
|
20
|
+
expect{ entry.test }.to raise_error("Couldn't find task type")
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#log' do
|
26
|
+
|
27
|
+
context 'task is already logged today' do
|
28
|
+
let(:options) do
|
29
|
+
{
|
30
|
+
'domain' => 'domain',
|
31
|
+
'username' => 'user',
|
32
|
+
'password' => 'password',
|
33
|
+
'project' => 'Pomodoro',
|
34
|
+
'task' => 'Ruby Development',
|
35
|
+
'name' => 'Template Refactoring'
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
before do
|
40
|
+
body = {
|
41
|
+
projects: [ {
|
42
|
+
name: 'Pomodoro',
|
43
|
+
id: 1,
|
44
|
+
tasks: [
|
45
|
+
{
|
46
|
+
name: 'Ruby Development',
|
47
|
+
id: 1
|
48
|
+
}
|
49
|
+
]
|
50
|
+
} ],
|
51
|
+
|
52
|
+
day_entries: [ {
|
53
|
+
notes: 'Template Refactoring',
|
54
|
+
project_id: 1,
|
55
|
+
task_id: 1,
|
56
|
+
hours: 1
|
57
|
+
} ]
|
58
|
+
}
|
59
|
+
|
60
|
+
stub_request(:get, /https:\/\/user:password@domain.harvestapp.com\/daily\/.*/).
|
61
|
+
to_return(:status => 200, :body => body.to_json, :headers => {})
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'updates exisiting entry' do
|
65
|
+
update_url = "https://user:password@domain.harvestapp.com/daily/update/"
|
66
|
+
body = {
|
67
|
+
notes: "Template Refactoring",
|
68
|
+
project_id: 1,
|
69
|
+
task_id: 1,
|
70
|
+
hours: 1.5,
|
71
|
+
spent_at: Date.today.strftime("%Y-%m-%d")
|
72
|
+
}
|
73
|
+
stub = stub_request(:put, update_url).with(:body => body.to_json)
|
74
|
+
|
75
|
+
entry = TomatoHarvest::TimeEntry.new(options)
|
76
|
+
entry.log(60 * 30)
|
77
|
+
|
78
|
+
stub.should have_been_requested
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
describe '#notes' do
|
86
|
+
|
87
|
+
it 'concats name and description' do
|
88
|
+
entry = described_class.new('name' => "Name")
|
89
|
+
expect(entry.notes).to eql("Name")
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
describe '#hours' do
|
95
|
+
|
96
|
+
it 'converts minutes to hours' do
|
97
|
+
expect(described_class.new.seconds_to_hours(60 * 60)).to be(1.00)
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'round to hundreths' do
|
101
|
+
expect(described_class.new.seconds_to_hours(61 * 60)).to be(1.02)
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe TomatoHarvest::Timer do
|
4
|
+
|
5
|
+
describe '.start' do
|
6
|
+
|
7
|
+
let(:task) { TomatoHarvest::Task.new('foo') }
|
8
|
+
|
9
|
+
before do
|
10
|
+
list = TomatoHarvest::List.new
|
11
|
+
list.add(task)
|
12
|
+
list.save
|
13
|
+
end
|
14
|
+
|
15
|
+
def stub_notifier(minutes)
|
16
|
+
message = "Pomodoro started for #{minutes} minutes"
|
17
|
+
options = {:title=>"TomatoHarvest", :subtitle=> 'foo'}
|
18
|
+
TerminalNotifier.should_receive(:notify).with(message, options)
|
19
|
+
|
20
|
+
message = "Pomodoro finished"
|
21
|
+
options = {:title=>"TomatoHarvest", :subtitle=> 'Pomodoro finished!'}
|
22
|
+
TerminalNotifier.should_receive(:notify).with(message, options)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'can run for a custom length' do
|
26
|
+
TomatoHarvest::Timer.start(task.id, minutes: 15)
|
27
|
+
|
28
|
+
reloaded_task = TomatoHarvest::List.find(task.id)
|
29
|
+
expect(reloaded_task.logged_minutes).to eql(15.0)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'can be run twice' do
|
33
|
+
TomatoHarvest::Timer.start(task.id, minutes: 20)
|
34
|
+
TomatoHarvest::Timer.start(task.id, minutes: 20)
|
35
|
+
reloaded_task = TomatoHarvest::List.find(task.id)
|
36
|
+
expect(reloaded_task.logged_minutes).to eql(40.0)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'logs a time entry if passed in' do
|
40
|
+
entry = double
|
41
|
+
entry.should_receive(:log)
|
42
|
+
TomatoHarvest::Timer.start(task.id, time_entry: entry, minutes: 25)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'tomatoharvest'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "tomatoharvest"
|
8
|
+
spec.version = TomatoHarvest::VERSION
|
9
|
+
spec.authors = ["Sam Reh"]
|
10
|
+
spec.email = ["samuelreh@gmail.com"]
|
11
|
+
spec.summary = %q{Log your pomodoros to Harvest}
|
12
|
+
spec.description = %q{Command line pomodoro timer that logs to Harvest.}
|
13
|
+
spec.homepage = "http://github.com/samuelreh/tomatoharvest/"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
spec.add_development_dependency "webmock"
|
25
|
+
|
26
|
+
spec.add_dependency('thor', '~> 0.19')
|
27
|
+
spec.add_dependency('harvested')
|
28
|
+
spec.add_dependency('daemons')
|
29
|
+
spec.add_dependency('terminal-notifier', '~> 1.4') if TomatoHarvest::OS.mac?
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tomatoharvest
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sam Reh
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-05-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: webmock
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: thor
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.19'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.19'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: harvested
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: daemons
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: terminal-notifier
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ~>
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.4'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ~>
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '1.4'
|
125
|
+
description: Command line pomodoro timer that logs to Harvest.
|
126
|
+
email:
|
127
|
+
- samuelreh@gmail.com
|
128
|
+
executables:
|
129
|
+
- toma
|
130
|
+
extensions: []
|
131
|
+
extra_rdoc_files: []
|
132
|
+
files:
|
133
|
+
- .gitignore
|
134
|
+
- Gemfile
|
135
|
+
- LICENSE.txt
|
136
|
+
- README.md
|
137
|
+
- Rakefile
|
138
|
+
- TODO
|
139
|
+
- bin/toma
|
140
|
+
- lib/tomatoharvest.rb
|
141
|
+
- lib/tomatoharvest/cli.rb
|
142
|
+
- lib/tomatoharvest/config.rb
|
143
|
+
- lib/tomatoharvest/list.rb
|
144
|
+
- lib/tomatoharvest/notifier.rb
|
145
|
+
- lib/tomatoharvest/notifier/notification_center.rb
|
146
|
+
- lib/tomatoharvest/os.rb
|
147
|
+
- lib/tomatoharvest/pomodoro.rb
|
148
|
+
- lib/tomatoharvest/task.rb
|
149
|
+
- lib/tomatoharvest/time_entry.rb
|
150
|
+
- lib/tomatoharvest/timer.rb
|
151
|
+
- lib/tomatoharvest/tmux.rb
|
152
|
+
- lib/tomatoharvest/version.rb
|
153
|
+
- spec/helper.rb
|
154
|
+
- spec/lib/tomatoharvest/cli_spec.rb
|
155
|
+
- spec/lib/tomatoharvest/config_spec.rb
|
156
|
+
- spec/lib/tomatoharvest/list_spec.rb
|
157
|
+
- spec/lib/tomatoharvest/time_entry_spec.rb
|
158
|
+
- spec/lib/tomatoharvest/timer_spec.rb
|
159
|
+
- tomatoharvest.gemspec
|
160
|
+
homepage: http://github.com/samuelreh/tomatoharvest/
|
161
|
+
licenses:
|
162
|
+
- MIT
|
163
|
+
metadata: {}
|
164
|
+
post_install_message:
|
165
|
+
rdoc_options: []
|
166
|
+
require_paths:
|
167
|
+
- lib
|
168
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - '>='
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '0'
|
173
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
174
|
+
requirements:
|
175
|
+
- - '>='
|
176
|
+
- !ruby/object:Gem::Version
|
177
|
+
version: '0'
|
178
|
+
requirements: []
|
179
|
+
rubyforge_project:
|
180
|
+
rubygems_version: 2.0.3
|
181
|
+
signing_key:
|
182
|
+
specification_version: 4
|
183
|
+
summary: Log your pomodoros to Harvest
|
184
|
+
test_files:
|
185
|
+
- spec/helper.rb
|
186
|
+
- spec/lib/tomatoharvest/cli_spec.rb
|
187
|
+
- spec/lib/tomatoharvest/config_spec.rb
|
188
|
+
- spec/lib/tomatoharvest/list_spec.rb
|
189
|
+
- spec/lib/tomatoharvest/time_entry_spec.rb
|
190
|
+
- spec/lib/tomatoharvest/timer_spec.rb
|
191
|
+
has_rdoc:
|