redpomo-reloaded 0.0.13
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Dockerfile +5 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +139 -0
- data/Guardfile +23 -0
- data/LICENSE +22 -0
- data/README.md +176 -0
- data/Rakefile +6 -0
- data/bin/redpomo +7 -0
- data/docker-compose.yml +7 -0
- data/lib/redpomo.rb +10 -0
- data/lib/redpomo/cli.rb +172 -0
- data/lib/redpomo/config.rb +26 -0
- data/lib/redpomo/entries_printer.rb +33 -0
- data/lib/redpomo/entry.rb +65 -0
- data/lib/redpomo/file_cache.rb +44 -0
- data/lib/redpomo/fuzzy_converter.rb +68 -0
- data/lib/redpomo/issue.rb +38 -0
- data/lib/redpomo/null_cache.rb +9 -0
- data/lib/redpomo/numeric_ext.rb +30 -0
- data/lib/redpomo/task.rb +103 -0
- data/lib/redpomo/task_list.rb +61 -0
- data/lib/redpomo/templates/config.yml +47 -0
- data/lib/redpomo/templates/issue_stub.textile +7 -0
- data/lib/redpomo/tracker.rb +175 -0
- data/lib/redpomo/ui.rb +73 -0
- data/lib/redpomo/version.rb +3 -0
- data/redpomo.gemspec +33 -0
- data/spec/file_cache_spec.rb +22 -0
- data/spec/fixtures/add_results.txt +4 -0
- data/spec/fixtures/cassettes/cli_add.yml +102 -0
- data/spec/fixtures/cassettes/cli_close.yml +50 -0
- data/spec/fixtures/cassettes/cli_pull.yml +1222 -0
- data/spec/fixtures/cassettes/cli_push.yml +297 -0
- data/spec/fixtures/cassettes/close_issue.yml +50 -0
- data/spec/fixtures/cassettes/create_issue.yml +102 -0
- data/spec/fixtures/cassettes/issues.yml +449 -0
- data/spec/fixtures/cassettes/push_entry.yml +55 -0
- data/spec/fixtures/close_results.txt +2 -0
- data/spec/fixtures/config.yml +16 -0
- data/spec/fixtures/printer_output.txt +16 -0
- data/spec/fixtures/proper_timelog.csv +4 -0
- data/spec/fixtures/pull_results.txt +20 -0
- data/spec/fixtures/tasks.txt +3 -0
- data/spec/fixtures/timelog.csv +6 -0
- data/spec/integration/add_spec.rb +29 -0
- data/spec/integration/init_spec.rb +33 -0
- data/spec/lib/redpomo/cli_spec.rb +91 -0
- data/spec/lib/redpomo/entry_spec.rb +23 -0
- data/spec/lib/redpomo/fuzzy_converter_spec.rb +65 -0
- data/spec/lib/redpomo/task_spec.rb +39 -0
- data/spec/lib/redpomo/tracker_spec.rb +72 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/cli_helpers.rb +76 -0
- data/spec/support/fixtures.rb +24 -0
- data/spec/support/ruby_ext.rb +20 -0
- data/spec/tmp/REDME.md +0 -0
- metadata +296 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
2
|
+
require 'active_support/core_ext/hash'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
require 'redpomo/file_cache'
|
6
|
+
require 'redpomo/null_cache'
|
7
|
+
|
8
|
+
module Redpomo
|
9
|
+
module Config
|
10
|
+
|
11
|
+
mattr_reader :todo_path, :trackers_data, :cache
|
12
|
+
|
13
|
+
@@cache = NullCache
|
14
|
+
|
15
|
+
def self.load_from_yaml(path)
|
16
|
+
config_path = File.expand_path(path)
|
17
|
+
return unless File.exists?(config_path)
|
18
|
+
data = YAML::load_file(config_path)
|
19
|
+
|
20
|
+
@@todo_path = File.expand_path(data["todo"], File.dirname(config_path))
|
21
|
+
@@trackers_data = data["trackers"].symbolize_keys!
|
22
|
+
@@cache = data["cache"] ? FileCache : NullCache
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'redpomo/numeric_ext'
|
2
|
+
|
3
|
+
module Redpomo
|
4
|
+
class EntriesPrinter
|
5
|
+
|
6
|
+
def self.print(entries)
|
7
|
+
require 'terminal-table'
|
8
|
+
entries.group_by(&:date).each do |date, entries|
|
9
|
+
duration = 0
|
10
|
+
rows = entries.map do |entry|
|
11
|
+
task = entry.to_task
|
12
|
+
duration += entry.duration
|
13
|
+
[
|
14
|
+
task.context,
|
15
|
+
task.project,
|
16
|
+
task.issue,
|
17
|
+
task.text,
|
18
|
+
entry.duration.seconds_in_words,
|
19
|
+
I18n.l(entry.time, format: "%H:%M"),
|
20
|
+
I18n.l(entry.end_time, :format => "%H:%M")
|
21
|
+
]
|
22
|
+
end
|
23
|
+
puts Terminal::Table.new(
|
24
|
+
title: "#{ I18n.l(date, format: "%A %x") } - #{ duration.seconds_in_words }",
|
25
|
+
headings: [ "Context", "Project", "Issue #", "Description", "Duration", "From", "To" ],
|
26
|
+
rows: rows
|
27
|
+
)
|
28
|
+
puts
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
module Redpomo
|
4
|
+
class Entry
|
5
|
+
|
6
|
+
def self.load_from_csv(text)
|
7
|
+
csv_rows(text).map do |data|
|
8
|
+
Entry.new(data[0], DateTime.parse(data[1]), data[2].to_i * 60.0) if data.size >= 3
|
9
|
+
end.compact.sort_by { |entry| entry.datetime }
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.csv_rows(text)
|
13
|
+
if text.match /^Export data created/
|
14
|
+
CSV.parse text.split("\n")[4..-1].join("\n")
|
15
|
+
else
|
16
|
+
CSV.parse text
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :text, :datetime, :duration
|
21
|
+
|
22
|
+
def initialize(text, datetime, duration)
|
23
|
+
@text = text
|
24
|
+
@datetime = datetime
|
25
|
+
@duration = duration
|
26
|
+
end
|
27
|
+
|
28
|
+
def date
|
29
|
+
datetime.to_date
|
30
|
+
end
|
31
|
+
|
32
|
+
def time
|
33
|
+
datetime.to_time
|
34
|
+
end
|
35
|
+
|
36
|
+
def end_time
|
37
|
+
time + duration
|
38
|
+
end
|
39
|
+
|
40
|
+
def same_date?(entry)
|
41
|
+
date == entry.date
|
42
|
+
end
|
43
|
+
|
44
|
+
def same_text?(entry)
|
45
|
+
text == entry.text
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_task
|
49
|
+
Task.new(nil, text)
|
50
|
+
end
|
51
|
+
|
52
|
+
def push!
|
53
|
+
tracker.push_entry!(self) if pushable?
|
54
|
+
end
|
55
|
+
|
56
|
+
def pushable?
|
57
|
+
tracker.present? && tracker.pushable_entry?(self)
|
58
|
+
end
|
59
|
+
|
60
|
+
def tracker
|
61
|
+
to_task.tracker
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'redpomo/config'
|
3
|
+
|
4
|
+
module Redpomo
|
5
|
+
class FileCache
|
6
|
+
|
7
|
+
include Singleton
|
8
|
+
attr_accessor :cache_path
|
9
|
+
|
10
|
+
def self.get(key, &block)
|
11
|
+
instance.get(key, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def get(key, &block)
|
15
|
+
return existing_keys[key] if existing_keys.has_key?(key)
|
16
|
+
if block_given?
|
17
|
+
value = block.call
|
18
|
+
set(key, value)
|
19
|
+
value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def initialize
|
26
|
+
@cache_path = File.expand_path("~/.redpomo-cache~")
|
27
|
+
end
|
28
|
+
|
29
|
+
def existing_keys
|
30
|
+
if File.exists?(cache_path)
|
31
|
+
YAML::load_file(cache_path) || {}
|
32
|
+
else
|
33
|
+
{}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def set(key, val)
|
38
|
+
dict = existing_keys
|
39
|
+
dict[key] = val
|
40
|
+
File.open(cache_path, 'w') { |f| f.write(dict.to_yaml) }
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'active_support/core_ext/numeric/time'
|
2
|
+
require 'redpomo/entry'
|
3
|
+
|
4
|
+
module Redpomo
|
5
|
+
class FuzzyConverter
|
6
|
+
|
7
|
+
def self.convert(entries)
|
8
|
+
past_entry = nil
|
9
|
+
consecutive_entries = []
|
10
|
+
|
11
|
+
entries << nil
|
12
|
+
|
13
|
+
result = []
|
14
|
+
|
15
|
+
entries.each do |entry|
|
16
|
+
if past_entry.present?
|
17
|
+
if entry.present? && entry.same_date?(past_entry) && entry.same_text?(past_entry)
|
18
|
+
consecutive_entries << entry
|
19
|
+
else
|
20
|
+
result << self.fuzzy_entry(consecutive_entries, entry)
|
21
|
+
past_entry = entry
|
22
|
+
consecutive_entries = [ entry ]
|
23
|
+
end
|
24
|
+
else
|
25
|
+
past_entry = entry
|
26
|
+
consecutive_entries = [ entry ]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
result
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.fuzzy_entry(entries, next_entry)
|
34
|
+
first_entry = entries.first
|
35
|
+
last_entry = entries.last
|
36
|
+
total_duration = first_entry.time - last_entry.end_time
|
37
|
+
duration_till_next_entry = next_entry.time - last_entry.end_time if next_entry.present?
|
38
|
+
|
39
|
+
entry = if next_entry.nil? || duration_till_next_entry > 1.5 * 3600.0
|
40
|
+
duration = last_entry.end_time - first_entry.time
|
41
|
+
duration += 1.5 * 3600.0 if next_entry.present? && next_entry.same_date?(last_entry)
|
42
|
+
Entry.new(first_entry.text, first_entry.datetime, duration)
|
43
|
+
else
|
44
|
+
duration = next_entry.time - first_entry.time
|
45
|
+
entry = Entry.new(first_entry.text, first_entry.datetime, duration)
|
46
|
+
end
|
47
|
+
entry
|
48
|
+
end
|
49
|
+
|
50
|
+
# def strip!
|
51
|
+
# end_working_day = Time.new(time.year, time.month, time.day, 18, 30, 0, "+00:00")
|
52
|
+
# start_launch_time = Time.new(time.year, time.month, time.day, 13, 15, 0, "+00:00")
|
53
|
+
# end_launch_time = Time.new(time.year, time.month, time.day, 14, 15, 0, "+00:00")
|
54
|
+
# late_night_day = Time.new(time.year, time.month, time.day, 23, 59, 0, "+00:00")
|
55
|
+
|
56
|
+
# if time < end_working_day && end_time > end_working_day
|
57
|
+
# @duration = end_working_day - time
|
58
|
+
# elsif time < start_launch_time && end_time > end_launch_time
|
59
|
+
# @duration -= 3600.0
|
60
|
+
# elsif time < start_launch_time && end_time > start_launch_time
|
61
|
+
# @duration = start_launch_time - time
|
62
|
+
# elsif time > end_working_day && end_time > late_night_day
|
63
|
+
# @duration = late_night_day - time
|
64
|
+
# end
|
65
|
+
# end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Redpomo
|
2
|
+
class Issue
|
3
|
+
|
4
|
+
attr_accessor :subject, :description
|
5
|
+
attr_accessor :issue_id, :project_id, :tracker
|
6
|
+
attr_accessor :due_date, :priority_id
|
7
|
+
|
8
|
+
def initialize(tracker, data = {})
|
9
|
+
@subject = data["subject"]
|
10
|
+
@issue_id = data["id"]
|
11
|
+
@project_id = data["project_id"]
|
12
|
+
@priority_id = data["priority"]["id"] if data["priority"].present?
|
13
|
+
@due_date = Date.parse(data["due_date"]) if data["due_date"].present?
|
14
|
+
@tracker = tracker
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_task
|
18
|
+
label = []
|
19
|
+
if @priority_id.present?
|
20
|
+
if priority = @tracker.todo_priority(@priority_id)
|
21
|
+
label << priority
|
22
|
+
end
|
23
|
+
end
|
24
|
+
label << @due_date.strftime("%Y-%m-%d") if @due_date.present?
|
25
|
+
label << @subject
|
26
|
+
label << "##{issue_id}"
|
27
|
+
label << "+#{project_id}"
|
28
|
+
label << "@#{tracker.name}"
|
29
|
+
Task.new(nil, label.join(" "))
|
30
|
+
end
|
31
|
+
|
32
|
+
def create!
|
33
|
+
data = tracker.create_issue!(self)
|
34
|
+
@issue_id = data["issue"]["id"]
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class Numeric
|
2
|
+
module Units
|
3
|
+
Sec = 1
|
4
|
+
Min = Sec * 60
|
5
|
+
Hour = Min * 60
|
6
|
+
Day = Hour * 24
|
7
|
+
Week = Day * 7
|
8
|
+
Month = Week * 4
|
9
|
+
Year = Day * 365
|
10
|
+
Decade = Year * 10
|
11
|
+
Century = Decade * 10
|
12
|
+
Millennium = Century * 10
|
13
|
+
Eon = 1.0/0
|
14
|
+
end
|
15
|
+
|
16
|
+
def seconds_in_words
|
17
|
+
return "0 secs" if self.zero?
|
18
|
+
unit = get_unit(self)
|
19
|
+
unit_difference = self / Units.const_get(unit.capitalize)
|
20
|
+
unit = unit.to_s.downcase + ('s' if self > 1)
|
21
|
+
"#{unit_difference.to_i} #{unit}"
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def get_unit(time_difference)
|
26
|
+
Units.constants.each_cons(2) do |con|
|
27
|
+
return con.first if (Units.const_get(con[0])...Units.const_get(con[1])) === time_difference
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/redpomo/task.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
2
|
+
require 'todo'
|
3
|
+
require 'redpomo/tracker'
|
4
|
+
|
5
|
+
module Todo
|
6
|
+
class Task
|
7
|
+
|
8
|
+
def self.projects_regex
|
9
|
+
/(?:\s+|^)\+[\w\-]+/
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module Redpomo
|
16
|
+
class Task
|
17
|
+
|
18
|
+
delegate :orig, to: :@task
|
19
|
+
delegate :priority, to: :@task
|
20
|
+
delegate :date, to: :@task
|
21
|
+
|
22
|
+
ISSUES_REGEXP = /(?:\s+|^)#[0-9]+/
|
23
|
+
|
24
|
+
def initialize(list, text)
|
25
|
+
@task = Todo::Task.new(text)
|
26
|
+
@list = list
|
27
|
+
end
|
28
|
+
|
29
|
+
def context
|
30
|
+
@task.contexts.map do |context|
|
31
|
+
context.gsub /^@/, ''
|
32
|
+
end.first
|
33
|
+
end
|
34
|
+
|
35
|
+
def project
|
36
|
+
@task.projects.map do |context|
|
37
|
+
context.gsub /^\+/, ''
|
38
|
+
end.first
|
39
|
+
end
|
40
|
+
|
41
|
+
def issue
|
42
|
+
orig.scan(ISSUES_REGEXP).map(&:strip).map do |issue|
|
43
|
+
issue.gsub(/^#/, '').to_i
|
44
|
+
end.first
|
45
|
+
end
|
46
|
+
|
47
|
+
def text
|
48
|
+
@task.text.gsub(ISSUES_REGEXP, '').strip
|
49
|
+
end
|
50
|
+
|
51
|
+
def close_issue!(message = nil)
|
52
|
+
tracker.close_issue!(issue, message)
|
53
|
+
end
|
54
|
+
|
55
|
+
def done!
|
56
|
+
@list.remove!(self)
|
57
|
+
end
|
58
|
+
|
59
|
+
def add!
|
60
|
+
TaskList.add!(self)
|
61
|
+
end
|
62
|
+
|
63
|
+
def open_in_browser!
|
64
|
+
require 'launchy'
|
65
|
+
Launchy.open(url)
|
66
|
+
end
|
67
|
+
|
68
|
+
def start_pomodoro!
|
69
|
+
require 'applescript'
|
70
|
+
command = 'tell application "Pomodoro" to start "'
|
71
|
+
command << orig
|
72
|
+
command << '"'
|
73
|
+
AppleScript.execute(command)
|
74
|
+
end
|
75
|
+
|
76
|
+
def url
|
77
|
+
return nil unless tracker.present?
|
78
|
+
if issue.present?
|
79
|
+
"#{tracker.base_url}/issues/#{issue}"
|
80
|
+
elsif project.present?
|
81
|
+
"#{tracker.base_url}/projects/#{project}"
|
82
|
+
else
|
83
|
+
"#{tracker.base_url}/projects/#{tracker.default_project}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def tracker
|
88
|
+
Tracker.find(context) if context.present?
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_issue
|
92
|
+
issue = Issue.new(tracker)
|
93
|
+
issue.subject = text
|
94
|
+
issue.project_id = project
|
95
|
+
issue.due_date = date
|
96
|
+
issue.priority_id = tracker.issue_priority_id(priority) if tracker.present?
|
97
|
+
issue
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'todo'
|
2
|
+
require 'redpomo/task'
|
3
|
+
|
4
|
+
module Redpomo
|
5
|
+
class TaskList < Array
|
6
|
+
|
7
|
+
def self.find(task_number)
|
8
|
+
list = TaskList.new(Config.todo_path)
|
9
|
+
list.find(task_number)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.pull_from_trackers!
|
13
|
+
list = TaskList.new(Config.todo_path)
|
14
|
+
list.pull_from_trackers!
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.add!(task)
|
18
|
+
list = TaskList.new(Config.todo_path)
|
19
|
+
list.add!(task)
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(path)
|
23
|
+
@path = path
|
24
|
+
File.read(path).split("\n").each do |line|
|
25
|
+
push Task.new(self, line)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def find(task_number)
|
30
|
+
slice(task_number.to_i - 1)
|
31
|
+
end
|
32
|
+
|
33
|
+
def remove!(task)
|
34
|
+
delete(task)
|
35
|
+
write!
|
36
|
+
end
|
37
|
+
|
38
|
+
def add!(task)
|
39
|
+
push task
|
40
|
+
write!
|
41
|
+
end
|
42
|
+
|
43
|
+
def pull_from_trackers!
|
44
|
+
issue_tasks = Tracker.all.map(&:issues).flatten.map(&:to_task)
|
45
|
+
delete_if do |task|
|
46
|
+
task.tracker.present?
|
47
|
+
end
|
48
|
+
self << issue_tasks
|
49
|
+
self.flatten!
|
50
|
+
write!
|
51
|
+
Redpomo.ui.info "Pulled #{issue_tasks.count} issues."
|
52
|
+
end
|
53
|
+
|
54
|
+
def write!
|
55
|
+
File.open(@path, 'w') do |file|
|
56
|
+
file.write map(&:orig).join("\n") + "\n"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|